Technology·15 min read

Expo DOM Components vs React Native WebView for Hybrid UIs

Embedding web content inside a native mobile app used to mean one thing: React Native WebView. Now Expo DOM components offer a fundamentally different approach. Here is how they compare and when each one wins.

Nate Laquis

Nate Laquis

Founder & CEO

The Hybrid UI Problem: Why You Need Web Content in Native Apps

Every mobile team eventually hits the same wall. You are building a React Native app, and you need to embed something that only exists in the web ecosystem. A rich text editor with collaborative editing. An interactive map with custom vector overlays. A payment form that requires PCI-compliant Stripe Elements. A charting library with 30 visualization types that would take six months to rebuild natively.

For years, the answer was always the same: reach for React Native WebView. Drop in a WebView component, point it at your web content, wire up postMessage for communication, and hope for the best. It worked. It also introduced a laundry list of problems: state synchronization headaches, styling inconsistencies, performance cliffs, debugging nightmares across two separate JavaScript contexts, and a fundamentally fragile communication layer built on string-serialized messages.

Expo DOM components, introduced with the "use dom" directive, represent a genuinely different approach. Instead of embedding a full browser instance and shouting across a message bridge, DOM components render React DOM content directly inside your React Native component tree. They share your build pipeline, accept props from native parents, and fire callbacks back without manual serialization. The experience is closer to rendering a React portal than embedding an iframe.

Mobile devices displaying hybrid native and web UI components side by side

But DOM components are not a drop-in replacement for every WebView use case. They have their own constraints, their own performance profile, and their own set of things they cannot do. Choosing the wrong one will cost you weeks. This guide breaks down both approaches with specific APIs, real performance numbers, and concrete recommendations so you can pick the right tool before you start building.

How Expo DOM Components Work: The 'use dom' Directive

If you have not used Expo DOM components yet, the mental model is simpler than you might expect. You add the "use dom" directive to the top of a React component file, and that component renders using React DOM instead of React Native primitives. On iOS and Android, Expo spins up a lightweight native web container for that component. On web, the directive is a no-op because you are already in a browser.

The Component Contract

A DOM component looks like any other React component from the outside. Your native parent passes props down. The DOM component renders HTML elements, uses CSS for styling, and can import web-only libraries like TipTap, D3.js, or Stripe Elements. When the component needs to communicate back to the native side, it calls callback functions received via props.

Here is the critical difference from WebView: props are passed directly through the bridge, not serialized into strings. Numbers stay numbers. Booleans stay booleans. Arrays stay arrays. Callback functions are proxied as async functions, so calling onChange(value) from your DOM component triggers the parent's handler without you manually constructing JSON payloads and parsing them on the other side.

Serialization Rules

DOM components accept serializable values: strings, numbers, booleans, arrays, and plain objects. Functions are proxied automatically. You cannot pass React elements, class instances, or complex objects with prototype chains. If you have worked with React Server Components, this boundary will feel familiar. The "use dom" boundary behaves almost identically to the RSC client/server boundary. For a deeper dive into the architecture, see our Expo DOM components universal apps guide.

Styling Independence

Inside a DOM component, you use real CSS. Tailwind, CSS Modules, styled-components, vanilla stylesheets. The component gets its own isolated styling context, so there is zero leakage between your React Native styles and your web styles. This isolation eliminates an entire class of bugs that plague WebView-based approaches, where global CSS from the embedded page bleeds into the host app's layout calculations.

The container auto-sizes to fit the DOM component's content, or you can set explicit dimensions from the native parent through style props. Layout negotiation between native and DOM layers happens automatically through the bridge, which is something you have to implement manually with WebView using injected JavaScript that reads document height and posts it back.

React Native WebView: The Traditional Approach

React Native WebView (react-native-webview) has been the standard solution for embedding web content since the early days of React Native. It wraps WKWebView on iOS and the Chromium-based WebView on Android, giving you a full browser instance inside your native app. Despite the rise of DOM components, WebView remains the right choice for certain scenarios, and understanding how it works helps you appreciate what DOM components actually improve.

Communication via postMessage

WebView communication is built on the postMessage API. Your native React code sends messages to the WebView using the injectedJavaScript prop or the injectJavaScript method. The WebView sends messages back using window.ReactNativeWebView.postMessage(). Every message is a string, so you end up wrapping everything in JSON.stringify on one side and JSON.parse on the other.

This works, but it is error-prone. There is no type safety across the boundary. If you change the shape of a message on one side and forget to update the other, you get silent failures at runtime. Teams typically build their own message protocol layer on top of postMessage, with action types, payloads, and response handlers. It is boilerplate that every WebView project reinvents.

Navigation Handling

WebView exposes a full navigation stack. The embedded web page can follow links, redirect, submit forms, and navigate freely. You control this with the onNavigationStateChange callback and the onShouldStartLoadWithRequest prop. For apps that embed existing web pages (like a terms-of-service page or a third-party portal), this navigation control is essential. DOM components do not have an equivalent because they are not loading pages. They are rendering components.

Injection Scripts

One powerful WebView feature is JavaScript injection. You can run arbitrary JavaScript inside the web context before or after the page loads. This lets you modify third-party pages, add event listeners to elements you do not control, override CSS, or extract data from pages that do not expose an API. This is the backbone of apps that integrate with external web services without official SDKs.

Code editor showing JavaScript injection and WebView bridge communication patterns

Cookie and Session Management

WebView gives you access to the full browser cookie jar. You can share authentication sessions between your WebView and web APIs, manage persistent cookies across app launches, and handle OAuth redirect flows entirely within the embedded browser. This makes WebView the go-to choice for apps that need to authenticate against third-party web services or embed authenticated web dashboards.

Head-to-Head Comparison: Developer Experience, Performance, and Architecture

Now that both approaches are on the table, here is how they compare across the dimensions that actually affect your shipping velocity and app quality.

Developer Experience

DOM components win this category decisively. You write a React component. You import web libraries. You pass props and receive callbacks. There is no message protocol to design, no JSON serialization to debug, and no injection scripts to maintain. The TypeScript compiler catches prop mismatches at build time. With WebView, you are maintaining two separate codebases (the native host and the web content) that communicate through untyped string messages. Refactoring either side requires manually verifying that the message contract still holds.

State Sharing

DOM components receive state as props from their React Native parent. Changes flow down through the standard React data model. When a DOM component needs to update parent state, it calls a callback prop. This is familiar React patterns, just across a bridge. You cannot share a Zustand store or React context directly across the boundary, but the prop/callback contract is typed and predictable.

WebView state sharing requires manual orchestration. You stringify state, post it as a message, parse it on the other side, and reverse the process for updates. For anything beyond trivial state, you end up building a synchronization layer. Race conditions are common when both sides try to update simultaneously, and debugging them requires inspecting message logs on both sides of the bridge. If your app relies on the React Native new architecture, the synchronous bridge makes native-side state sharing even faster, but the WebView boundary remains async and string-based regardless.

Performance

DOM components run inside a lightweight web container that Expo manages. The initial overhead is roughly 800KB to 1.2MB for the first DOM component, with incremental cost per additional component based on its web dependencies. Bridge round-trip latency sits around 2 to 5ms for prop updates and callback invocations.

WebView launches a full browser process. On iOS, WKWebView runs in a separate process with its own memory space. On Android, the WebView is in-process but still initializes a full Chromium rendering engine. Cold start for a WebView is typically 200 to 400ms on modern devices, compared to 50 to 100ms for a DOM component's first render. Memory overhead is higher too: a WebView typically consumes 30 to 60MB of RAM depending on content complexity, while a DOM component with equivalent content uses 10 to 25MB because it does not initialize the full browser chrome, navigation stack, and caching layer.

Debugging

DOM components can be debugged through Expo's standard tooling. Because the component is part of your Expo build pipeline, source maps work, breakpoints hit, and React DevTools can inspect the component tree. WebView debugging requires connecting Safari DevTools (for iOS) or Chrome DevTools (for Android) separately, switching between two debugger windows, and correlating events across two JavaScript runtimes. It is doable but slow, especially when tracking down issues in the message-passing layer.

Bundle Size Impact

DOM components add the web runtime once, then each component adds only its own dependencies. If you use three DOM components that all import React DOM, they share a single React DOM bundle. WebView itself adds minimal bundle size (the native WebView is part of the OS), but any web content you load still needs to be bundled, hosted, or fetched at runtime. If you are loading local HTML, you bundle it as an asset. If you are loading remote URLs, you take a network dependency at render time.

Use Cases: Picking the Right Tool for Specific Scenarios

Abstract comparisons only get you so far. Here is how each approach performs for the specific use cases that drive most hybrid UI decisions.

Rich Text Editors

DOM components are the clear winner. Libraries like TipTap, Lexical, and ProseMirror are battle-tested web editors with years of development behind them. Wrapping TipTap in a DOM component gives you collaborative editing, custom formatting toolbars, markdown shortcuts, and slash commands in a couple of days. You pass the initial content as a prop, receive onChange callbacks with the updated HTML or JSON, and the editor just works. With WebView, you would load the editor in an HTML page, communicate content changes through postMessage, and handle focus, selection, and keyboard events across the bridge manually.

Charts and Data Visualization

DOM components again. D3.js, Recharts, Highcharts, and Chart.js render beautifully inside a DOM component. You pass data arrays as props, and the chart renders with web-native performance. Interactivity (tooltips, zoom, drill-down) stays inside the DOM component. With WebView, you would need to serialize large datasets as JSON strings, inject them into the page, and handle interaction events through postMessage. For a dashboard with five chart types and 10,000 data points, the DOM component approach is roughly three times faster to implement and produces noticeably smoother interactions.

Third-Party Web Portals and Authenticated Sessions

WebView wins here. If you need to embed an existing web application that you do not control (a banking portal, an insurance claim form, a partner dashboard), WebView is your only option. You load the URL, manage cookies for authentication, handle navigation events, and optionally inject CSS to customize the appearance. DOM components cannot load arbitrary URLs because they render React components, not web pages.

Payment Forms

DOM components have the edge for Stripe Elements, Braintree Drop-in, and similar component-based payment SDKs. These libraries provide React components that you render inside a DOM component with full PCI compliance. The payment token comes back through a callback prop. WebView can do this too (by loading a hosted payment page), but the DOM component approach gives you tighter integration and avoids the redirect-based flow that WebView payments often require.

Maps with Custom Overlays

This one depends. If you need a basic map with markers and routes, use react-native-maps natively. Skip both WebView and DOM components. But if you need Mapbox GL JS with custom vector tile layers, 3D terrain, or complex GeoJSON overlays that rely on web-specific rendering APIs, a DOM component wrapping Mapbox GL JS gives you the full feature set without the limitations of the native Mapbox SDK bindings.

Development team collaborating on hybrid mobile app architecture in a modern office

Existing Web Components Migration

DOM components are purpose-built for this. If you are converting a web app to React Native and want to migrate incrementally, DOM components let you wrap existing React web components and embed them directly in native screens. You migrate one component at a time, replacing DOM components with native equivalents when it makes sense. WebView can technically do this too (by loading your web app in a frame), but you lose type-safe communication and end up maintaining two deployment pipelines.

Migration Path: Moving from WebView to DOM Components

If you have an existing app using React Native WebView and you want to migrate to DOM components where it makes sense, here is a practical migration strategy that minimizes risk.

Step 1: Audit Your WebView Usage

List every place your app uses WebView and categorize each one. Is it loading a remote URL you do not control? That stays as WebView. Is it loading local HTML that renders a React-based widget? That is a migration candidate. Is it a simple HTML rendering scenario (displaying formatted content from a CMS)? That might not need either. React Native's built-in Text component with basic formatting, or a library like react-native-render-html, could handle it without any web context at all.

Step 2: Extract the Web Logic

For each migration candidate, extract the web-side logic into a standalone React component. If your WebView loads an HTML file that initializes a chart library, rewrite that as a proper React component that accepts data as props and renders the chart. This is the hardest step because WebView-based code often mixes concerns: DOM manipulation, event handling, message parsing, and rendering logic all tangled together. Take the time to separate them cleanly.

Step 3: Create the DOM Component

Add the "use dom" directive to your new component file. Import the web libraries directly (no more script tags in HTML strings). Replace postMessage communication with prop/callback patterns. Replace injected CSS with proper stylesheets or CSS-in-JS. Replace window.ReactNativeWebView.postMessage calls with callback prop invocations.

Step 4: Run Both in Parallel

Before ripping out the WebView, run the DOM component alongside it behind a feature flag. Compare behavior, performance, and edge cases. Pay special attention to keyboard handling (DOM components and WebView handle the software keyboard differently on Android), scroll behavior (nested scrolling conflicts are common with both approaches), and memory consumption under load. Once you are confident the DOM component matches the WebView's behavior, remove the old code.

Limitations to Know Before You Migrate

DOM components cannot do everything WebView can. You should know the boundaries before committing to a migration. DOM components cannot load arbitrary URLs. They render React components, not web pages. If your WebView navigates to external sites, you need to keep WebView for that. DOM components do not support cookies or web storage in the way a full browser does. If your WebView relies on persistent cookies for session management, you will need to handle authentication through your native layer and pass tokens as props. DOM components cannot inject JavaScript into content you do not control. If your WebView modifies third-party pages, there is no DOM component equivalent. Finally, DOM components on native platforms run in an isolated JavaScript context. You cannot access the native app's AsyncStorage, SecureStore, or file system from inside a DOM component. Data flows in through props and out through callbacks, nothing else. Check the Expo vs bare React Native comparison for more context on how these architectural boundaries affect your overall app structure.

When to Use Each: A Decision Framework

After shipping over a dozen hybrid apps that use one or both of these approaches, here is the decision framework we use at Kanopy Labs.

Use Expo DOM Components When:

  • You control the web content. You are rendering your own React components, not loading third-party pages. The "use dom" directive requires a React component file that you own and maintain.
  • You need tight native integration. Your web content needs to receive data from native state, trigger native actions, or update in response to native events. DOM components handle this through typed props and callbacks with no custom protocol needed.
  • Performance matters. The lighter runtime, faster cold start, and lower memory footprint of DOM components make a measurable difference on mid-range devices. If your users are on $200 Android phones, every megabyte of RAM counts.
  • You want a single codebase. DOM components are part of your Expo project. They build with the same pipeline, share TypeScript types, and deploy with the same OTA update mechanism. No separate web hosting, no versioning mismatches.
  • You are embedding web ecosystem libraries. Rich text editors, charting libraries, payment forms, code editors, markdown renderers. These are DOM component sweet spots.

Use React Native WebView When:

  • You are loading content you do not control. Third-party web portals, partner dashboards, OAuth flows, terms-of-service pages. Anything where you need a real browser to load a real URL.
  • You need full browser capabilities. Cookie management, navigation history, back/forward controls, download handling, file uploads through the browser's native file picker.
  • You need to modify third-party pages. JavaScript injection lets you customize the appearance and behavior of pages you did not build. DOM components have no equivalent to this.
  • You are building a browser-like experience. In-app browsers, web content viewers, documentation readers that load remote content. These are inherently WebView use cases.
  • Your web content has its own deployment pipeline. If your web team deploys the embedded content independently and you want the app to always load the latest version, a WebView pointed at a URL decouples the deployment cycles.

Use Both Together

These approaches are not mutually exclusive. A sophisticated app might use DOM components for its rich text editor and charting views while using WebView for an embedded third-party support portal and OAuth login flow. The key is matching each piece of web content to the approach that fits its requirements. Do not force everything through one tool because it is simpler to reason about. The complexity savings of using the right tool for each job outweigh the complexity of maintaining two approaches.

If you are building a hybrid mobile app and want expert guidance on architecting the native/web boundary, our team has shipped these patterns across dozens of production apps. Book a free strategy call and we will walk through your specific use case, recommend the right approach, and help you avoid the pitfalls we have already mapped out.

Need help building this?

Our team has launched 50+ products for startups and ambitious brands. Let's talk about your project.

Expo DOM Components vs WebView comparisonExpo DOM componentsReact Native WebViewhybrid mobile UIweb content in native apps

Ready to build your product?

Book a free 15-minute strategy call. No pitch, just clarity on your next steps.

Get Started