Edit this page
Learn about rendering React DOM elements in Expo native apps.
Edit this page
Experimentally available in SDK 52 and above. To try out the latest DOM components features, you can install the canary release.
Warning: DOM components are an experimental feature. The public API (expo/dom
,dom
prop), prop transport system, asset handling, and export embedding system are subject to breaking changes. UseWebViews
directly for a production-ready approach.
Expo offers a novel approach to work with modern web code directly in a native app via the 'use dom'
directive. This enables incremental migration for an entire website to a universal app by moving on a per-component basis.
While the Expo native runtime generally does not support elements like <div>
or <img>
, there may be instances where you need to quickly incorporate web components. In such cases, DOM components provide a useful solution.
Install react-native-webview
in your project:
-Â
npx expo install react-native-webview
To render a React component to the DOM, add the 'use dom'
directive to the top of the web component file:
'use dom';
export default function DOMComponent({ name }: { name: string }) {
return (
<div>
<h1>Hello, {name}</h1>
</div>
);
}
Inside the native component file, import the web component to use it:
import DOMComponent from './my-component.tsx';
export default function App() {
return (
// This is a DOM component. It re-exports a wrapped `react-native-webview` behind the scenes.
<DOMComponent name="Europa" />
);
}
To pass props to the underlying native WebView, add a dom
object to the component:
'use dom';
export default function DOMComponent({}: { dom: import('expo/dom').DOMProps }) {
return (
<div>
<h1>Hello, world!</h1>
</div>
);
}
Now you can pass WebView
props to the DOM component:
import DOMComponent from './my-component';
export default function App() {
return (
<DOMComponent
dom={{
scrollEnabled: false,
}}
/>
);
}
You can send data to the DOM component through serializable props (number
, string
, boolean
, null
, undefined
, Array
, Object
). For example, inside a native component file, you can pass a prop to the DOM component:
import DOMComponent from './my-component';
export default function App() {
return <DOMComponent hello={'world'} />;
}
Inside the web component file, you can receive the prop as shown in the example below:
'use dom';
export default function DOMComponent({ hello }: { hello: string }) {
return <p>Hello, {hello}</p>;
}
Props are sent over an asynchronous bridge so they are not updated synchronously. They are passed as props to the React root component, which means they re-render the entire React tree.
You can send type-safe native functions to DOM components by passing asynchronous functions as top-level props to the DOM component:
import DomComponent from './my-component';
export default function App() {
return (
<DomComponent
hello={(data: string) => {
console.log('Hello', data);
}}
/>
);
}
'use dom';
export default function MyComponent({ hello }: { hello: (data: string) => Promise<void> }) {
return <p onClick={() => hello('world')}>Click me</p>;
}
You cannot pass functions as nested props to DOM components. They must be top-level props.
Native actions are always asynchronous and accept only serializable arguments (meaning no functions) because the data is sent over a bridge to the DOM component's JavaScript engine.
Native actions can return serializable data to the DOM component, which is useful for getting data back from the native side.
getDeviceName(): Promise<string> {
return DeviceInfo.getDeviceName();
}
Think of these functions like React Server Actions, but instead of residing on the server, they live locally in the native app and communicate with the DOM component. This approach provides a powerful way to add truly native functionality to your DOM components.
Since DOM components are used to run websites, you might need extra qualifiers to better support certain libraries. You can detect if a component is running in a DOM component with the following code:
const IS_DOM = typeof ReactNativeWebView !== 'undefined';
The contents of the root public directory are copied to the native app's binary to support the use of public assets in DOM components. Since these public assets will be served from the local filesystem, use the process.env.EXPO_DOM_BASE_URL
prefix to reference the correct path. For example:
<img src={`${process.env.EXPO_DOM_BASE_URL}/img.png`} />
By default, all console.log
methods are extended in WebViews to forward logs to the terminal. This makes it fast and easy to see what's happening in your DOM components.
Expo also enables WebView inspection and debugging when bundling in development mode. You can open Safari > Develop > Simulator > MyComponent.tsx to see the webview's console and inspect elements.
You can create a manual WebView using the WebView
component from react-native-webview
. This can be useful for rendering websites from a remote server.
import { WebView } from 'react-native-webview';
export default function App() {
return <WebView source={{ html: '<h1>Hello, world!</h1>' }} />;
}
You may want to measure the size of a DOM component and report it back to the native side (for example, native scrolling). This can be done using a autoSize
prop or a manual native action:
autoSize
propYou need to use the dom={{ autoSize: true }}
prop to measure the size of the DOM component automatically and resize the native view coresponsingly:
import DOMComponent from './my-component';
export default function Route() {
return <DOMComponent dom={{ autoSize: true }} />;
}
'use dom';
export default function DOMComponent(_: { dom?: import('expo/dom').DOMProps }) {
return <div style={{ width: 500, height: 500, backgroundColor: 'blue' }} />;
}
You can also manually measure the size of a DOM component and report it back to the native side using a native action:
import DOMComponent from './my-component';
import { useState } from 'react';
export default function Route() {
const [height, setHeight] = useState(270);
return (
<DOMComponent
updateSize={async size => {
if (size[1] !== height) {
setHeight(size[1]);
}
}}
dom={{
style: { height },
}}
/>
);
}
'use dom';
import { useEffect } from 'react';
function useSize(callback: (size: [number, number]) => void) {
useEffect(() => {
// Observe window size changes
const observer = new ResizeObserver(entries => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
callback([width, height]);
}
});
observer.observe(document.body);
callback([document.body.clientWidth, document.body.clientHeight]);
return () => {
observer.disconnect();
};
}, [callback]);
}
export default function DOMComponent({
onLayout,
}: {
dom?: import('expo/dom').DOMProps;
onLayout(size: [number, number]);
}) {
useSize(onLayout);
return <div />;
}
Built-in DOM support only renders websites as single-page applications (no SSR or SSG). This is because search engine optimization and indexing are unnecessary for embedded JS code.
When a module is marked with 'use dom'
, it is replaced with a proxy reference imported at runtime. This feature is primarily achieved through a series of bundler and CLI techniques. You can always use WebView with standard approach by passing raw HTML to a WebView
component.
If desired, you can use a WebView with the standard approach by passing raw HTML to a WebView component.
DOM components rendered within websites or other DOM components will behave as regular components, and the dom
prop will be ignored. This is because web content is passed directly through and not wrapped in an iframe
.
Overall, this system shares many similarities with Expo's React Server Components implementation.
We recommend building truly native apps using universal primitives such as View
, Image
, and Text
. DOM Components only support standard JavaScript, which is slower to parse and start up than optimized Hermes bytecode.
Data can be sent between DOM components and native components only through an asynchronous JSON transport system. Avoid relying on data across JS engines and deep linking to nested URLs in DOM components, as they do not currently support full reconciliation with Expo Router.
While DOM Components are not exclusive to Expo Router, they are developed and tested against Expo Router apps to provide the best experience when used with Expo Router.
If you have a global state for sharing data, it will not be accessible across JS engines.
While native modules in the Expo SDK can be optimized to support DOM Components, this optimization has not been implemented yet. Use native actions and props to share native functionality with DOM components.
DOM Components and websites in general are less optimal than native views but there are some reasonable uses for them. For example, the web is conceptually the best way to render rich-text and markdown. The web also has very good WebGL support, with the caveat that devices in low-power mode will often throttle web frame rates to preserve battery.
Many large apps also use some web content for auxiliary routes such as blog posts, rich-text (for example, long-form posts on X), settings pages, help pages, and other less frequently visited parts of the app.
DOM Components currently only render as single-page applications and don't support static rendering or React Server Components (RSC). When the project uses React Server Components,'use dom'
will work the same as 'use client'
regardless of the platform. RSC Payloads can be passed as properties to DOM Components. However, they cannot be hydrated correctly on native platforms as they'll be rendered for a native runtime.
children
to DOM components.Ultimately, universal architecture is the most exciting kind. Expo CLI's extensive universal tooling is the only reason we can even offer a feature as intricate and valuable as this one.
While DOM components help with migration and moving quickly, we recommend using truly native views whenever possible.