Skip to content

Instantly share code, notes, and snippets.

@smontlouis
Last active June 18, 2026 10:11
Show Gist options
  • Select an option

  • Save smontlouis/4f73e9acf8244f6a4d14f22e831f9d0a to your computer and use it in GitHub Desktop.

Select an option

Save smontlouis/4f73e9acf8244f6a4d14f22e831f9d0a to your computer and use it in GitHub Desktop.
Expo 56 DOM Components with react-native-webview patches
diff --git a/build/src/start/server/middleware/DomComponentsMiddleware.js b/build/src/start/server/middleware/DomComponentsMiddleware.js
index 9354bedfc84d53efd54e089ad3e9e23c4d026123..55d8eb166ff1cfa17aa2a34623b01ca9e97dd583 100644
--- a/build/src/start/server/middleware/DomComponentsMiddleware.js
+++ b/build/src/start/server/middleware/DomComponentsMiddleware.js
@@ -132,13 +132,22 @@ function getDomComponentHtml(src, { title } = {}) {
<script>${_domPolyfills.DOM_POLYFILLS_SCRIPT}</script>
<script>
var injectedObject = {};
- try {
+ if (window.ReactNativeWebView && typeof window.ReactNativeWebView.injectedObjectJson === 'function') {
+ try {
injectedObject = JSON.parse(window.ReactNativeWebView.injectedObjectJson());
- } catch (e) {
- throw new Error('Failed to parse injectedObjectJson: ' + e.message);
+ } catch (e) {
+ console.error('Failed to parse injectedObjectJson: ' + e.message);
+ }
}
- window.$$EXPO_DOM_HOST_OS = injectedObject.EXPO_DOM_HOST_OS;
- window.$$EXPO_INITIAL_PROPS = injectedObject.initialProps;
+ var fallbackHostOS = /Android/i.test(navigator.userAgent)
+ ? 'android'
+ : /iPhone|iPad|iPod/i.test(navigator.userAgent)
+ ? 'ios'
+ : 'web';
+ var hasInitialProps = !!injectedObject.initialProps || typeof window.$$EXPO_INITIAL_PROPS !== 'undefined';
+ window.$$EXPO_DOM_HOST_OS = injectedObject.EXPO_DOM_HOST_OS || window.$$EXPO_DOM_HOST_OS || fallbackHostOS;
+ window.$$EXPO_INITIAL_PROPS = injectedObject.initialProps || window.$$EXPO_INITIAL_PROPS || { names: [], props: {} };
+ window.$$EXPO_INITIAL_PROPS_PENDING = !hasInitialProps;
</script>
${src ? `<script crossorigin src="${src.replace(/^https?:/, '')}"></script>` : ''}
</body>

Expo 56 DOM Components with react-native-webview

This gist documents a workaround for Expo 56 DOM Components when using:

<MyDOMComponent
  dom={{
    useExpoDOMWebView: false,
  }}
/>

The goal is to keep Expo DOM Components, but run them through react-native-webview instead of @expo/dom-webview.

Why not use @expo/dom-webview?

In this project, @expo/dom-webview was not usable in production because of Android bridge failures such as:

Error: Call to function 'DomWebView.injectJavaScript' has been rejected.
-> Caused by: The 1st argument cannot be cast to type class expo.modules.webview.DomWebView (received class java.lang.Integer)
-> Caused by: Unable to find the class expo.modules.webview.DomWebView view with tag 1028

On iOS, @expo/dom-webview also did not expose the same control surface as react-native-webview; in particular, it did not let us remove the iOS keyboard accessory toolbar.

So the preferred setup was:

dom={{
  useExpoDOMWebView: false,
}}

That switches Expo DOM Components to react-native-webview.

What breaks in Expo 56?

Expo DOM Components expect bootstrap data to exist before the DOM bundle starts:

  • window.$$EXPO_DOM_HOST_OS
  • window.$$EXPO_INITIAL_PROPS

In production, with useExpoDOMWebView: false, the generated DOM HTML may assume that this function exists:

window.ReactNativeWebView.injectedObjectJson()

But with react-native-webview, that method is not always available early enough. This can produce errors like:

Failed to parse injectedObjectJson: window.ReactNativeWebView.injectedObjectJson is not a function
Top OS ($$EXPO_DOM_HOST_OS) is not defined. This is a bug in the DOM Component runtime.

After adding a simple fallback, another race can appear: the DOM component may render once with empty fallback props, causing app-level crashes such as:

Cannot read properties of undefined (reading 'colors')

That happens because the component rendered before the real marshalled props arrived.

What the patch does

There are two Yarn patches:

  1. expo-npm-56.0.12.patch

    • injects $$EXPO_DOM_HOST_OS and $$EXPO_INITIAL_PROPS through injectedJavaScriptBeforeContentLoaded;
    • re-sends the current props on WebView onLoad;
    • teaches expo/dom/entry to render null while fallback props are marked as pending.
  2. @expo-cli-npm-56.1.16.patch

    • patches the generated DOM HTML;
    • stops throwing when injectedObjectJson() is missing;
    • adds an OS fallback from the user agent;
    • creates minimal fallback props;
    • marks fallback props as pending so the DOM component waits for the real $$props message before rendering.
diff --git a/src/dom/dom-entry.tsx b/src/dom/dom-entry.tsx
index 46f25f77f998a67a3b560afad7eca2dfc35c8185..2ff61462cf984b3b50a4d8548f7d15cfc5f695ea 100644
--- a/src/dom/dom-entry.tsx
+++ b/src/dom/dom-entry.tsx
@@ -67,18 +67,22 @@ export function registerDOMComponent(AppModule: any) {
function DOMComponentRoot(props: Record<string, unknown>) {
// Props listeners
- const [marshalledProps, setProps] = React.useState(() => {
+ const [marshalledProps, setProps] = React.useState<MarshalledProps | null>(() => {
if (typeof window.$$EXPO_INITIAL_PROPS === 'undefined') {
throw new Error(
'Initial props are not defined. This is a bug in the DOM Component runtime.'
);
}
+ if ((window as any).$$EXPO_INITIAL_PROPS_PENDING) {
+ return null;
+ }
return window.$$EXPO_INITIAL_PROPS;
});
React.useEffect(() => {
const remove = addEventListener!((msg) => {
if (msg.type === '$$props') {
+ (window as any).$$EXPO_INITIAL_PROPS_PENDING = false;
setProps(msg.data as MarshalledProps);
}
});
@@ -88,14 +92,18 @@ export function registerDOMComponent(AppModule: any) {
}, [setProps]);
const proxyActions = React.useMemo(() => {
- if (!marshalledProps.names) return {};
+ if (!marshalledProps?.names) return {};
// Create a named map { [name: string]: ProxyFunction }
// TODO(@kitten): Unclear how this is typed or shaped
return marshalledProps.names.reduce((acc: Record<string, any>, key: string) => {
acc[key] = ACTIONS[key];
return acc;
}, {});
- }, [marshalledProps.names]);
+ }, [marshalledProps?.names]);
+
+ if (!marshalledProps) {
+ return null;
+ }
return <AppModule {...props} {...(marshalledProps.props || {})} {...proxyActions} />;
}
diff --git a/src/dom/webview-wrapper.tsx b/src/dom/webview-wrapper.tsx
index 1695ebcf983f50949f4b7dce2845669f6b159383..a61d707f29a22ccf049182db4431f8f29bf1bf3c 100644
--- a/src/dom/webview-wrapper.tsx
+++ b/src/dom/webview-wrapper.tsx
@@ -106,6 +106,16 @@ const RawWebView = React.forwardRef<object, Props>((props, ref) => {
// Keep `initialProps` stable to prevent webview reloads when
// `injectedJavaScriptObject` changes.
const initialPropsRef = React.useRef(smartActions);
+ const initialDomGlobalsScript = React.useMemo(() => {
+ const hostOS = JSON.stringify(process.env.EXPO_OS);
+ const initialProps = JSON.stringify(initialPropsRef.current);
+ return `;(function() {
+ window.$$EXPO_DOM_HOST_OS = ${hostOS};
+ window.$$EXPO_INITIAL_PROPS = ${initialProps};
+ window.$$EXPO_INITIAL_PROPS_PENDING = false;
+ })();
+ true;`;
+ }, []);
// When the `marshalProps` change, emit them to the webview.
React.useEffect(() => {
@@ -144,12 +154,23 @@ const RawWebView = React.forwardRef<object, Props>((props, ref) => {
...(useExpoDOMWebView ? { useExpoModulesBridge } : null),
containerStyle: [containerStyle, debugZeroHeightStyle, dom?.containerStyle],
onLayout: (__DEV__ ? debugOnLayout : dom?.onLayout) as RawWebViewProps['onLayout'],
+ onLoad: (event: any) => {
+ dom?.onLoad?.(event);
+ emit({ type: '$$props', data: smartActions });
+ },
injectedJavaScriptObject: {
// Inject the top-most OS for the DOM component to read.
EXPO_DOM_HOST_OS: process.env.EXPO_OS,
// Inject the initial props
initialProps: initialPropsRef.current,
},
+ injectedJavaScriptBeforeContentLoaded: [
+ initialDomGlobalsScript,
+ dom?.injectedJavaScriptBeforeContentLoaded,
+ 'true;',
+ ]
+ .filter(Boolean)
+ .join('\n'),
injectedJavaScript: [
dom?.matchContents ? getInjectBodySizeObserverScript() : null,
dom?.injectedJavaScript,
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment