Illustrated technology atlas · React Native and Expo JS/native interoperability on Android and iOS

React Native + Expo: across the JS/native boundary

Best practices

For software engineers. New Architecture, Hermes, Fabric, TurboModules and Expo Modules; Android/iOS call paths, threading, lifecycle, development builds and update compatibility. Conceptual API examples; no pinned SDK version or benchmark claims. Legacy bridge is historical context.

Research date: 2026-10-08

Part 1 · Fundamentals

One app, three layers

Prerequisites: Start here / basic technical literacy

One app, three layers — mechanism

Conceptual layers. The module path is distinct from rendering; all arrows are interface relationships, not guaranteed threads. Nodes: React + TS, Hermes, JSI / RN C++, Android / iOS, Expo Modules.
Conceptual layers. The module path is distinct from rendering; all arrows are interface relationships, not guaranteed threads. [S01] [S03] [S06] [S10]

Mental model verified

Your TS/React logic executes in a JS engine, usually Hermes. React Native hosts it inside an Android or iOS app. C++ infrastructure and platform-specific code connect the JS runtime to native APIs and views. Expo is a toolkit and module layer on this architecture, not an additional rendering engine.

[S01] [S06] [S10]

Worked example synthesis

A camera screen has React state and JSX in JS, a native camera view on Android/iOS, and an Expo or third-party module that exposes camera methods and events to JS. Permission prompts and camera capture live in platform code.

[S06] [S07]

Engineering choice synthesis

First look for a maintained native library. Write a small module only for the unsupported platform capability. Audit where data is copied and what thread owns it.

[S06] [S07]

Pitfall and next step synthesis

Do not call every native interaction “the bridge.” Trace a single button press through JS, module or renderer, and platform API; identify what returns synchronously and what returns later.

[S01] [S07]
Keep this: React Native is a JS application coordinated with a C++ renderer and platform code; Expo supplies a higher-level module and build workflow.
Check yourself: Where does Hermes run, and does Expo add another JS runtime?

Hermes runs the application JavaScript. Expo modules expose native capabilities into that same React Native app; Expo is not a second application JS runtime.

Part 2 · Fundamentals

What JSI changed

Prerequisites: 01-three-layers

What JSI changed — mechanism

Two conceptual interop paths; the lower path removes mandatory bridge serialization, not every copy or scheduling cost. Nodes: Legacy JS, Serialized queue, Native, New JS, C++ host object, Native.
Two conceptual interop paths; the lower path removes mandatory bridge serialization, not every copy or scheduling cost. [S01] [S07]

Mental model verified

The legacy bridge queued serialized messages between JS and native. In the New Architecture, JSI lets JS interact with C++ host objects through an engine-independent interface. Native module frameworks build typed or declarative APIs above it.

[S01] [S07]

Worked example synthesis

A synchronous getCachedValue() may return a small cached scalar. A synchronous readHugeFile() would block the JS caller until I/O finishes; put I/O in an asynchronous function. This is an illustrative design example.

[S07]

Engineering choice synthesis

Keep synchronous APIs bounded and predictable. Pass identifiers or handles for large native assets where a library supports them; measure conversion and end-to-end latency rather than guessing from call count.

[S01] [S07]

Pitfall and next step synthesis

“Zero-copy everywhere” is false: JSI permits native references but APIs can still convert or copy values. Profile the real path and check ownership and lifetime of referenced native data.

[S01] [S07]
Keep this: JSI allows direct runtime bindings and native object references, but synchronous calls still consume caller time.
Check yourself: Does “no old bridge” mean there is no boundary cost?

No. JSI avoids mandatory JSON-style message serialization, yet conversion, allocation, locks, scheduling and native work can still cost time.

Part 3 · Fundamentals

Fabric: from JSX to pixels

Prerequisites: 01-three-layers

Fabric: from JSX to pixels — mechanism

The common rendering path. Scheduling can differ; the final host-view mutation belongs on the UI thread. Nodes: JSX + state, Shadow tree, Commit + Yoga, Mount, Host views.
The common rendering path. Scheduling can differ; the final host-view mutation belongs on the UI thread. [S02] [S03] [S04]

Mental model verified

Fabric is the React Native renderer. It builds an immutable C++ shadow tree for host components, calculates layout, diffs the next tree, and mounts host views. Render and commit may be scheduled in several ways; the UI thread owns host view manipulation.

[S02] [S03] [S04]

Worked example synthesis

Changing a card title creates affected React elements and shadow nodes, then commits layout and applies a host view update. The native control is not rebuilt from scratch for every React render.

[S03]

Engineering choice synthesis

Use normal declarative props for view state. A native view is justified when a platform SDK owns a complex surface, such as a map or camera preview. Keep events and props small and well typed.

[S03] [S07]

Pitfall and next step synthesis

Do not equate React render with immediate on-screen paint. Draw the render → commit → mount phases and inspect a slow frame for JS work, layout, or UI-thread mount cost.

[S02] [S03] [S12]
Keep this: Render creates a C++ shadow tree, commit computes layout, and mount changes host views on the UI thread.
Check yourself: What thread can actually change an Android View or iOS UIView?

The UI/main thread performs the host view mutation during mount.

Part 4 · Applications

TurboModule: typed service API

Prerequisites: 02-jsi-versus-bridge

TurboModule: typed service API — mechanism

A nonvisual native service contract. Language details and generated glue vary by module implementation. Nodes: TS/Flow spec, Codegen, Android, iOS.
A nonvisual native service contract. Language details and generated glue vary by module implementation. [S05] [S11]

Mental model verified

A TurboModule exposes functions and events. A TypeScript or Flow spec describes types, Codegen emits platform glue, and native Android/iOS implementations fulfill that contract. Fabric components handle native views separately.

[S05] [S11]

Worked example synthesis

Define getBatteryLevel(): Promise<number> in a typed module spec, implement the platform query separately in Kotlin and Swift/Objective-C++, and handle rejection or missing capability in JS. This is pseudocode for contract shape, not copy-paste setup.

[S05]

Engineering choice synthesis

Choose TurboModule when you need RN Codegen contracts or lower-level C++ integration. Keep the exported surface small and versioned; errors and nullability belong in the API design.

[S05] [S06]

Pitfall and next step synthesis

Generated types do not make platform behavior identical. Test permission denial, lifecycle and device-specific availability on both platforms.

[S05] [S11]
Keep this: A typed JS specification and Codegen tie a native service contract to Android and iOS implementations.
Check yourself: Is a TurboModule the path for custom native visual components?

Use a native component/Fabric view for visual surfaces; a TurboModule exposes nonvisual native functions and events.

Part 5 · Applications

Expo Modules: one API, two platforms

Prerequisites: 02-jsi-versus-bridge

Expo Modules: one API, two platforms — mechanism

The same JS interface leads to separately implemented platform behavior; Expo is an abstraction over the RN boundary. Nodes: JS/TS wrapper, Expo Modules API, Swift module, Kotlin module.
The same JS interface leads to separately implemented platform behavior; Expo is an abstraction over the RN boundary. [S06] [S07]

Mental model verified

Expo Modules API abstracts JSI and related RN primitives. A module definition names the JS-visible surface. Function executes synchronously on the JS runtime thread; AsyncFunction returns a Promise and by default dispatches native work away from that thread.

[S06] [S07]

Worked example synthesis

A biometric SDK wrapper could expose isAvailable as a small cached query and authenticateAsync as an asynchronous operation. Swift and Kotlin implement the same JS-facing intent while each platform handles its own permission and result behavior.

[S07]

Engineering choice synthesis

Prefer the Expo DSL for a Swift/Kotlin integration when its API fits. Use a TurboModule when the module needs direct C++ access or RN Codegen contracts. Profile before attributing bottlenecks to either framework.

[S05] [S06]

Pitfall and next step verified

AsyncFunction does not mean “runs on the UI thread.” Explicitly choose the main queue for UI work; view-bound async methods have their own main-thread behavior. Define failure and cancellation semantics.

[S07]
Keep this: Expo’s Swift/Kotlin DSL exposes Function, AsyncFunction, events and views over React Native’s low-level primitives.
Check yourself: When should an Expo module use AsyncFunction instead of Function?

For I/O, long work, or work that must be dispatched to another queue; a synchronous Function runs on the JS caller thread.

Part 6 · Best practices

Threads: keep the caller responsive

Prerequisites: 03-fabric-path, 05-expo-module-contract

Threads: keep the caller responsive — mechanism

Queue ownership for an illustrative operation; the exact dispatch path depends on the module API and implementation. Nodes: JS thread, Worker / native queue, UI/main thread, Promise / event.
Queue ownership for an illustrative operation; the exact dispatch path depends on the module API and implementation. [S02] [S07] [S12]

Mental model verified

The JS thread runs application logic and usually render work; the UI thread alone manipulates host views. React Native may schedule some rendering on the UI thread. Expo Function runs on JS; AsyncFunction can dispatch to a native queue, with .runOnQueue(.main) when UI access is required.

[S02] [S07]

Worked example synthesis

For a document picker: start platform UI on main, perform file reading away from main, resolve the Promise with a URI and metadata, then update React state on JS. Avoid sending the whole file through a JS value when a URI suffices.

[S02] [S07]

Engineering choice synthesis

Bound synchronous work, use async for I/O, and isolate native state touched from multiple queues. Keep main-thread sections short and check cancellation when an activity or view goes away.

[S07] [S12]

Pitfall and next step synthesis

A Promise can still hide blocking native work if its implementation uses the main queue for CPU-heavy processing. Trace queue hops with profiling and measure both JS and UI frames in a release build.

[S07] [S12]
Keep this: Decide on a queue from the work itself: short sync work on JS, platform UI mutations on main, slow work away from both.
Check yourself: Why can a native synchronous method cause a dropped JS frame?

It executes on the JS caller thread and prevents JS work from progressing until it returns.

Part 7 · Best practices

Native events need a lifecycle

Prerequisites: 04-turbomodule-contract, 05-expo-module-contract

Native events need a lifecycle — mechanism

Event lifecycle and data flow. Aggregation and event ordering must be designed for the particular producer. Nodes: Native producer, Subscription, Event payload, JS listener, Cleanup.
Event lifecycle and data flow. Aggregation and event ordering must be designed for the particular producer. [S07] [S11]

Mental model verified

Native modules can emit events to JS; Expo Modules provide Events, OnStartObserving and OnStopObserving. The native producer may keep working after a screen disappears unless listener and lifecycle cleanup are explicit.

[S07] [S11]

Worked example synthesis

A location feed begins when the first listener subscribes, emits compact coordinates at a useful cadence, and stops on the final unsubscribe. When the app backgrounds, decide whether collection should pause based on product requirements and platform rules.

[S07]

Engineering choice synthesis

Coalesce high-frequency telemetry, send bounded payloads, and define ordering, missing-event and resubscription behavior. Use native processing when each sample need not reach JS.

[S07] [S11]

Pitfall and next step synthesis

Sending every sensor sample to React state can overwhelm rendering. Measure event rate and JS frame drops; reduce frequency or aggregate natively when the UI only needs a summary.

[S07] [S12]
Keep this: Events are a subscription with cleanup and throughput policy, not an unlimited stream of JS callbacks.
Check yourself: What should happen when the final JS listener is removed?

Stop the native observation or release resources if no other owner needs them.

Part 8 · Best practices

The native binary is an API contract

Prerequisites: 05-expo-module-contract

The native binary is an API contract — mechanism

Build-time native surface and update-time JS surface. An OTA update cannot add a new compiled native module. Nodes: App config, Prebuild / compile, Installed binary, OTA update, runtimeVersion.
Build-time native surface and update-time JS surface. An OTA update cannot add a new compiled native module. [S08] [S09]

Mental model verified

Expo Go contains a fixed native library set. A development build includes your chosen native packages. CNG generates Android and iOS projects from config; config plugins describe persistent changes to native project files. A runtimeVersion links OTA updates to compatible binaries.

[S08] [S09]

Worked example synthesis

Adding a native payment SDK changes the binary. Install it, configure its plugin, create and test new Android/iOS development and production builds, then publish JS updates only to the matching runtime.

[S08] [S09]

Engineering choice verified

Treat every native dependency/config change as a release compatibility event. Use a runtimeVersion policy and test a preview build with the same native runtime before rollout.

[S09]

Pitfall and next step verified

Editing generated AndroidManifest.xml or Info.plist directly in a CNG workflow can be lost on prebuild. Encode changes in config plugins and check generated projects on both platforms.

[S08]
Keep this: A JS update can only use native APIs already compiled into a compatible binary.
Check yourself: Why does adding a native package require a new app build?

Its native implementation is absent from binaries already installed; an OTA JS update cannot compile that code into them.

Part 9 · Further improvements

Measure before moving work native

Prerequisites: 06-thread-ownership, 07-events-and-lifecycle

Measure before moving work native — mechanism

A measurement loop, not a claim that native code is inherently faster for a particular workload. Nodes: Symptom, Trace, Hypothesis, Change, Re-measure.
A measurement loop, not a claim that native code is inherently faster for a particular workload. [S01] [S10] [S12]

Mental model verified

A slow screen may be caused by JS computation, too many renders, layout, UI-thread work, image decoding or native callback volume. JSI reduces one class of overhead but is not a universal performance fix. Hermes behavior and profiling should be judged in release builds.

[S01] [S10] [S12]

Worked example synthesis

If a camera overlay stutters, record JS and UI frame behavior, event frequency and payload size. Try lower event cadence or native aggregation first; compare the same device and scenario after each change.

[S07] [S12]

Engineering choice synthesis

Set a target interaction and trace a real flow. Change one bottleneck at a time; record median and tail latency, dropped frames and memory on representative Android and iOS devices. These are proposed measurements, not claimed results.

[S01] [S12]

Pitfall and next step synthesis

A microbenchmark for module calls can miss image conversion, native SDK work and React rerender cost. Validate the user-visible result and watch for regressions in startup, battery or memory.

[S01] [S10] [S12]
Keep this: Measure JS frames, UI frames, payload size and native work before deciding which boundary to change.
Check yourself: What evidence would support moving a per-frame calculation to native?

A release-build trace showing repeated boundary or JS work hurting the target frame budget, plus an improved end-to-end measurement after a prototype.

Part 10 · Further improvements

One JS API, honest platform behavior

Prerequisites: 05-expo-module-contract, 08-builds-and-ota

One JS API, honest platform behavior — mechanism

The public API can be shared; platform lifecycle and capability checks remain separate. Nodes: JS contract, Android path, iOS path, Shared tests, Device checks.
The public API can be shared; platform lifecycle and capability checks remain separate. [S07] [S08] [S09]

Mental model verified

An API can present the same JS method name on both platforms while Android Activity lifecycle and iOS app lifecycle hooks differ. A good abstraction states common behavior and surfaces capability differences explicitly.

[S07]

Worked example synthesis

For a secure-document scan wrapper, define a discriminated result for success, user cancellation, denied permission and unsupported capability. Implement dismissal/cleanup separately for Android and iOS, then test each path on devices.

[S07]

Engineering choice synthesis

Keep the TS facade stable; make platform-specific native tests and contract tests for returned shapes. Version the binary and JS API together when native behavior changes.

[S07] [S09]

Pitfall and next step synthesis

A simulator success can hide hardware permission and lifecycle problems. Add device checks for background/foreground, rotation or activity recreation where relevant, and app termination.

[S07] [S08]
Keep this: Share the public contract, then test platform-specific permission, lifecycle, threading and error behavior independently.
Check yourself: Does the same method name guarantee identical Android and iOS semantics?

No. Native capabilities and lifecycles differ; the wrapper should document differences and normalize only what can be normalized honestly.

Sources & further reading

  1. [S01] About the New Architecture

    React Native · official documentation · accessed 2026-10-08 · current documentation as accessed

    Supports: JSI replaces the serialized bridge; enabling it alone does not promise speed.

    Read the diagrams and API boundaries for jsi replaces the serialized bridge; enabling it alone does not promise speed.

  2. [S02] Threading Model

    React Native · official documentation · accessed 2026-10-08 · current documentation as accessed

    Supports: JS and UI thread responsibilities; render can run in more than one scheduling scenario.

    Read the diagrams and API boundaries for js and ui thread responsibilities; render can run in more than one scheduling scenario.

  3. [S03] Render, Commit, and Mount

    React Native · official documentation · accessed 2026-10-08 · current documentation as accessed

    Supports: Fabric render, commit, layout and host view mount phases.

    Read the diagrams and API boundaries for fabric render, commit, layout and host view mount phases.

  4. [S04] Fabric

    React Native · official documentation · accessed 2026-10-08 · current documentation as accessed

    Supports: Cross-platform C++ renderer and host platform interoperability.

    Read the diagrams and API boundaries for cross-platform c++ renderer and host platform interoperability.

  5. [S05] Native Modules: Introduction

    React Native · official documentation · accessed 2026-10-08 · current documentation as accessed

    Supports: Typed TS or Flow spec and platform-specific Codegen interfaces.

    Read the diagrams and API boundaries for typed ts or flow spec and platform-specific codegen interfaces.

  6. [S06] Expo Modules API: Overview

    Expo · official documentation · accessed 2026-10-08 · current documentation as accessed

    Supports: Expo Modules API uses Swift and Kotlin and supports the New Architecture.

    Read the diagrams and API boundaries for expo modules api uses swift and kotlin and supports the new architecture.

  7. [S07] Expo Modules API: Reference

    Expo · official documentation · accessed 2026-10-08 · current documentation as accessed

    Supports: JSI abstraction; Function, AsyncFunction, queues, views, events and lifecycle.

    Read the diagrams and API boundaries for jsi abstraction; function, asyncfunction, queues, views, events and lifecycle.

  8. [S08] Add custom native code

    Expo · official documentation · accessed 2026-10-08 · current documentation as accessed

    Supports: Expo Go limits, development builds, CNG and config plugins.

    Read the diagrams and API boundaries for expo go limits, development builds, cng and config plugins.

  9. [S09] Runtime versions and updates

    Expo · official documentation · accessed 2026-10-08 · current documentation as accessed

    Supports: OTA JS update compatibility with the compiled native runtime.

    Read the diagrams and API boundaries for ota js update compatibility with the compiled native runtime.

  10. [S10] Using Hermes

    React Native · official documentation · accessed 2026-10-08 · current documentation as accessed

    Supports: Bundled Hermes is the default JS engine and release builds matter for measurement.

    Read the diagrams and API boundaries for bundled hermes is the default js engine and release builds matter for measurement.

  11. [S11] Emitting Events in Native Modules

    React Native · official documentation · accessed 2026-10-08 · current documentation as accessed

    Supports: Native-to-JS events in TurboModules.

    Read the diagrams and API boundaries for native-to-js events in turbomodules.

  12. [S12] Performance Overview

    React Native · official documentation · accessed 2026-10-08 · current documentation as accessed

    Supports: JS and UI frame rates and practical performance diagnosis.

    Read the diagrams and API boundaries for js and ui frame rates and practical performance diagnosis.