--- name: use-dom description: Use Expo DOM components to run web code in a webview on native and as-is on web. Migrate web code to native incrementally. version: 1.0.0 license: MIT --- ## What are DOM Components? DOM components allow web code to run verbatim in a webview on native platforms while rendering as-is on web. This enables using web-only libraries like `recharts`, `react-syntax-highlighter`, or any React web library in your Expo app without modification. ## When to Use DOM Components Use DOM components when you need: - **Web-only libraries** — Charts (recharts, chart.js), syntax highlighters, rich text editors, or any library that depends on DOM APIs - **Migrating web code** — Bring existing React web components to native without rewriting - **Complex HTML/CSS layouts** — When CSS features aren't available in React Native - **iframes or embeds** — Embedding external content that requires a browser context - **Canvas or WebGL** — Web graphics APIs not available natively ## When NOT to Use DOM Components Avoid DOM components when: - **Native performance is critical** — Webviews add overhead - **Simple UI** — React Native components are more efficient for basic layouts - **Deep native integration** — Use local modules instead for native APIs - **Layout routes** — `_layout` files cannot be DOM components ## Basic DOM Component Create a new file with the `'use dom';` directive at the top: ```tsx // components/WebChart.tsx "use dom"; export default function WebChart({ data, }: { data: number[]; dom: import("expo/dom").DOMProps; }) { return (

Chart Data

); } ``` ## Rules for DOM Components 1. **Must have `'use dom';` directive** at the top of the file 2. **Single default export** — One React component per file 3. **Own file** — Cannot be defined inline or combined with native components 4. **Serializable props only** — Strings, numbers, booleans, arrays, plain objects 5. **Include CSS in the component file** — DOM components run in isolated context ## The `dom` Prop Every DOM component receives a special `dom` prop for webview configuration. Always type it in your props: ```tsx "use dom"; interface Props { content: string; dom: import("expo/dom").DOMProps; } export default function MyComponent({ content }: Props) { return
{content}
; } ``` ### Common `dom` Prop Options ```tsx // Disable body scrolling // Flow under the notch (disable safe area insets) // Control size manually // Combine options ``` ## Exposing Native Actions to the Webview Pass async functions as props to expose native functionality to the DOM component: ```tsx // app/index.tsx (native) import { Alert } from "react-native"; import DOMComponent from "@/components/dom-component"; export default function Screen() { return ( { Alert.alert("From Web", message); }} saveData={async (data: { name: string; value: number }) => { // Save to native storage, database, etc. console.log("Saving:", data); return { success: true }; }} /> ); } ``` ```tsx // components/dom-component.tsx "use dom"; interface Props { showAlert: (message: string) => Promise; saveData: (data: { name: string; value: number; }) => Promise<{ success: boolean }>; dom?: import("expo/dom").DOMProps; } export default function DOMComponent({ showAlert, saveData }: Props) { const handleClick = async () => { await showAlert("Hello from the webview!"); const result = await saveData({ name: "test", value: 42 }); console.log("Save result:", result); }; return ; } ``` ## Using Web Libraries DOM components can use any web library: ```tsx // components/syntax-highlight.tsx "use dom"; import SyntaxHighlighter from "react-syntax-highlighter"; import { docco } from "react-syntax-highlighter/dist/esm/styles/hljs"; interface Props { code: string; language: string; dom?: import("expo/dom").DOMProps; } export default function SyntaxHighlight({ code, language }: Props) { return ( {code} ); } ``` ```tsx // components/chart.tsx "use dom"; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, } from "recharts"; interface Props { data: Array<{ name: string; value: number }>; dom: import("expo/dom").DOMProps; } export default function Chart({ data }: Props) { return ( ); } ``` ## CSS in DOM Components CSS imports must be in the DOM component file since they run in isolated context: ```tsx // components/styled-component.tsx "use dom"; import "@/styles.css"; // CSS file in same directory export default function StyledComponent({ dom, }: { dom: import("expo/dom").DOMProps; }) { return (

Styled Content

); } ``` Or use inline styles / CSS-in-JS: ```tsx "use dom"; const styles = { container: { padding: 20, backgroundColor: "#f0f0f0", }, title: { fontSize: 24, color: "#333", }, }; export default function StyledComponent({ dom, }: { dom: import("expo/dom").DOMProps; }) { return (

Styled Content

); } ``` ## Expo Router in DOM Components The expo-router `` component and router API work inside DOM components: ```tsx "use dom"; import { Link, useRouter } from "expo-router"; export default function Navigation({ dom, }: { dom: import("expo/dom").DOMProps; }) { const router = useRouter(); return ( ); } ``` ### Router APIs That Require Props These hooks don't work directly in DOM components because they need synchronous access to native routing state: - `useLocalSearchParams()` - `useGlobalSearchParams()` - `usePathname()` - `useSegments()` - `useRootNavigation()` - `useRootNavigationState()` **Solution:** Read these values in the native parent and pass as props: ```tsx // app/[id].tsx (native) import { useLocalSearchParams, usePathname } from "expo-router"; import DOMComponent from "@/components/dom-component"; export default function Screen() { const { id } = useLocalSearchParams(); const pathname = usePathname(); return ; } ``` ```tsx // components/dom-component.tsx "use dom"; interface Props { id: string; pathname: string; dom?: import("expo/dom").DOMProps; } export default function DOMComponent({ id, pathname }: Props) { return (

Current ID: {id}

Current Path: {pathname}

); } ``` ## Detecting DOM Environment Check if code is running in a DOM component: ```tsx "use dom"; import { IS_DOM } from "expo/dom"; export default function Component({ dom, }: { dom?: import("expo/dom").DOMProps; }) { return
{IS_DOM ? "Running in DOM component" : "Running natively"}
; } ``` ## Assets Prefer requiring assets instead of using the public directory: ```tsx "use dom"; // Good - bundled with the component const logo = require("../assets/logo.png"); export default function Component({ dom, }: { dom: import("expo/dom").DOMProps; }) { return Logo; } ``` ## Usage from Native Components Import and use DOM components like regular components: ```tsx // app/index.tsx import { View, Text } from "react-native"; import WebChart from "@/components/web-chart"; import CodeBlock from "@/components/code-block"; export default function HomeScreen() { return ( Native content above Native content below ); } ``` ## Platform Behavior | Platform | Behavior | | -------- | ----------------------------------- | | iOS | Rendered in WKWebView | | Android | Rendered in WebView | | Web | Rendered as-is (no webview wrapper) | On web, the `dom` prop is ignored since no webview is needed. ## Tips - DOM components hot reload during development - Keep DOM components focused — don't put entire screens in webviews - Use native components for navigation chrome, DOM components for specialized content - Test on all platforms — web rendering may differ slightly from native webviews - Large DOM components may impact performance — profile if needed - The webview has its own JavaScript context — cannot directly share state with native