react-brain
Browse decisions
React & language foundations 3App architecture 11UI 13Platform & native 9Build, test, observe, secure 6AI in React apps 3stack composerdecision recordscensusstaleness benchnewsletterarchitecturechangelogmethodologyroadmap

entriesapp-architecture · verified 2026-09-24 · react + react-native

Sharing code across React (web) & React Native

reviewedconfidence: highthis tier holds 83% on the public scorecard (6/11 graded · 0 overturned) →

related decisions: ai-devtools

cited by: nav · desktop · styling · component-libs

re-verified 4× — 2026-09-24 · 2026-09-21 · 2026-07-13 · 2026-07-06 · changelog

recommendation

Shared logic/hooks package (a platform-agnostic workspace package used by both the React web app and the React Native app) first — the highest-leverage, lowest-risk win. For shared UI on NEW projects the pick is Expo Universal Components or React Strict DOM, not a fresh react-native-web bet. The pick changes only for an organisation that can staff and gate agent-maintained duplicate native implementations (see when).

  • duplicated business logic across web + native → shared logic/hooks package (extract a workspace package now)
  • UI that must differ per platform behind one API → platform-extension files (.web.tsx/.native.tsx)
  • new universal UI in an Expo app → Expo Universal Components (@expo/ui; native lanes stable, web lane experimental)
  • new universal UI authored as strict HTML/CSS, web-first → React Strict DOM (pre-1.0, involved setup)
  • existing react-native-web app → react-native-web (keep it; still works, just don't expand the bet)
  • existing Tamagui design system → Tamagui (keep it; cross-platform UI with an optimizing compiler)
  • Expo + Next.js shared routing → Solito
  • incremental web reuse inside an RN app → Expo DOM Components (`use dom`)
  • native-only mobile org that can staff and gate agent-maintained duplicate Swift/Kotlin implementations → share the spec, not the code (Shopify's route; see note: SHOPIFY)

Options & tradeoffs

the field considered — and why each one isn’t the default here

optiontradeoff
shared logic/hooks packageplatform-agnostic core (business logic, state, data) in a workspace package; serves the web app and the native app alike — the highest-leverage, lowest-risk share
platform-extension files (.web.tsx/.native.tsx)one API, per-platform implementation; keeps each file single-platform instead of Platform.OS branching
React Strict DOM (Meta)write strict HTML/CSS rendering web + native; Babel flattens to real DOM + static CSS on web; emerging RN-Web successor; involved setup. 0.0.55 (2026-01-09, verified vs npm), pre-1.0
react-native-webMAINTENANCE MODE (2026) — maps RN primitives to HTML; still works, no major features coming. 0.21.2 (2025-10-16, verified vs npm); see note: REACT-NATIVE-WEB
Tamaguicross-platform UI + optimizing compiler
Expo Universal Components / @expo/uisingle API → SwiftUI (iOS) / Jetpack Compose (Android) / react-dom or react-native-web (web); native APIs stable since SDK 56, web APIs still experimental (verified vs the SDK 56 changelog); see note: EXPO UI
Solitoshared routing across Expo + Next.js; v5 dropped the RN-Web core dep
Expo DOM Components (`use dom`)render real web components inside RN via a webview wrapper (since Expo SDK 52); good for incremental web reuse

Verified notes

Share by layer. Logic, state and data are platform-agnostic, so a shared workspace package is the highest-leverage, lowest-risk win — and in a React web + React Native codebase it also serves the web app. When logic or components are duplicated across a web app and a native app, that duplication is the prime target. UI that diverges per platform is split by platform-extension files so each file stays single-platform; widespread inline Platform.OS branching means sharing happens at the wrong layer. Universal UI is picked only on a maintained lane. REACT-NATIVE-WEB (2026, verified vs official RN sources, Callstack and Gallagher's notes): react-native-web has entered MAINTENANCE-ONLY mode — creator Nicolas Gallagher and Zalando pivoted to React Strict DOM, and Software Mansion sees "no major features on the horizon". → NEW universal web+native work goes to React Strict DOM or Expo Universal Components; existing RN-Web apps keep working. EXPO UI (2026-09-24, verified vs the SDK 56 changelog): SDK 56 made the Expo UI native APIs stable and added universal components across Android, iOS and web, backed by @expo/ui/jetpack-compose, @expo/ui/swift-ui and react-dom or react-native-web; the web APIs are still experimental and likely to change. → Expo apps get a universal-UI lane without splitting files; treat its web half as unstable. SHOPIFY (2026-09-21, verified vs shopify.engineering/back-to-native): Shopify is moving its mobile apps off React Native to Swift and Kotlin. It adopted RN in 2020 to stop building features twice, let developers work across the stack and stop chasing parity; all three delivered. Its reason to leave: coding models made "building the same feature in Swift and Kotlin" no longer carry "the cost it used to", so sharing an implementation lost value while platform closeness and first-party tooling did not. It does not claim RN is slow ("React Native apps can be fast. Ours are") or was wrong ("React Native was the right choice for Shopify in 2020"). Its replacement is the new option in this design space: duplicate the implementation per platform and hold parity through shared specifications, tests and review checkpoints that agents execute, with an agent implementing a feature on Android using the iOS version as a reference — "share the spec, not the code". The default still holds on this entry's terms: (1) the default's claim is about LOGIC, and two agent-maintained copies are cheaper than before but not free ("that cost has not disappeared"); (2) the migration is not a swap a normal team can copy — greenfield rebuilds, a bespoke checkpoint-gated system (Helix), and business logic re-architected to run headless for agents; (3) this entry's scope includes the web app, which a shared logic package also serves and which Shopify's decision does not weigh. → A team with that machinery has a real alternative to sharing code; a team without it still pays full price for duplicated logic. The revisit trigger is a second organisation of that size publishing the same conclusion. CROSS-REFERENCES: RB-E-AI-DEVTOOLS owns the agent-workflow half of the Shopify thread (Helix, headless agent-drivable business logic). The engineering-principles skill owns boundaries and dependency direction for the shared package. HISTORY: 2026-09-24 CORRECTED: the Expo UI row said "SDK 56 beta"; SDK 56 made the native APIs stable and left only the web APIs experimental. 2026-09-21 CHALLENGED → SURVIVES: Shopify's move off RN attacked the share-an-implementation premise with "share the spec, not the code" (agent-maintained duplicates); default holds, competitor named as a when-clause.

Canonical reading

Editorial annotations on why each piece matters — the articles themselves are the originals; read them there.

How Does KMP with SwiftUI Scale on iOS?Andrei Calazans

The best-measured statement of the ALTERNATIVE this entry's axis implies — share the logic layer, keep the UI native — taken to production scale on Kotlin Multiplatform: ~750 Kotlin and ~700 Swift files, all figures measured wall-clock on an M4 Pro with Xcode 26.5 / Kotlin 2.4.0 / Gradle 9.6.1 rather than estimated. Two structural facts are the transferable part. (1) iOS consumes the shared layer as ONE linked framework no matter how many Gradle modules it is split into, because every linked KMP framework embeds its own copy of the Kotlin/Native runtime, memory manager and GC — ship two and you get two uncoordinated collectors in one process, with Kotlin objects crossing a boundary neither manages, so the single umbrella framework is the SAFE shape and its link step is the price of that safety, not a tooling accident. 'Just split the framework' is therefore not a free lever. (2) The decision he rates highest is that Swift feature packages compile against Swift CONTRACTS (closure-struct protocols), not against the Kotlin framework — only a thin SharedBridge imports it — which is the same insulation principle this entry applies to platform-extension files. Useful whether or not you would pick KMP: it prices the seam that any share-the-logic architecture has to own.

use dom — incrementally migrating web UI to native with ReactEvan Bacon (Expo)

Deep-dive on Expo DOM Components and the `use dom` directive — run real web UI inside a native app and migrate to native views component-by-component; covers the Metro bundling, offline support, and honest WebView trade-offs. The primary source for the incremental-reuse path.

AI-Assisted React Native Migration for TV: Lessons From ZattooCallstack (Zattoo case study)

TV/cross-platform RN case study: share until a platform gives a reason to split, telemetry-driven perf, team reorg, and a deflated, realistic account of AI's role. A concrete companion to this entry's share-by-layer thesis.

Sources

Depth (in-domain rules) is owned by the engineering-principles skill — this entry is selection breadth.

The full explanation

The reviewed long-form essay behind this entry — the why, not a how-to. Also on GitHub.

About sharing code across React (web) & React Native

Diataxis: Explanation. This is the conceptual core of why react-brain exists: when a team maintains both a web app and a native app, duplicated logic is the prime target. This page explains how to think about sharing and why the recommendation is shaped the way it is. The option list lives in the index entry RB-E-CROSSPLATFORM.

"Write once, run anywhere" is the wrong frame

The seductive promise is one codebase for web and native. The durable reality is subtler: some layers share almost for free, and one layer — UI — fights you. Teams that try to share everything hit an abstraction tax (leaky Platform.select everywhere, lowest- common-denominator components) and conclude "cross-platform doesn't work." Teams that share by layer get most of the benefit with little of the pain. So the real question is not "share or not" but "which layers, and how far up the stack?"

The layered model (the mental model to keep)

Think of an app as a stack, sharing-friendliness decreasing as you go up:

  1. Logic & hooks — pure functions, business rules, validation, formatting, custom hooks. Platform-agnostic by nature. Share first; this is the highest-leverage, lowest- risk win.
  2. State & data — client state (RB-E-STATE) and the data layer (RB-E-DATA). The leading libraries (Zustand, TanStack Query) are already platform-agnostic, so this layer shares cleanly — put it in the common package.
  3. UI — components and styling. This is where platforms genuinely differ (navigation patterns, gestures, native controls, motion), and where "share everything" goes wrong. Share here only with eyes open, using one of the dedicated approaches below.

The single most useful action for most teams is unglamorous: extract a platform-agnostic logic/hooks package (layers 1–2) into the monorepo and consume it from both apps. That alone removes most duplication and the bugs that come from two drifting copies — and it's reversible and low-risk. Depth on package boundaries and dependency direction is owned by engineering-principles.

The default, and why

Shared logic/hooks package (a platform-agnostic workspace package used by both the React web app and the React Native app) first — the highest-leverage, lowest-risk win. For shared UI on new projects the pick is Expo Universal Components or React Strict DOM, not a fresh react-native-web bet. The pick changes only for an organisation that can staff and gate agent-maintained duplicate native implementations.

The UI clause carries a verified, load-bearing 2026 shift (below). The ordering — logic before UI — is deliberate: it front-loads the cheap, certain wins and defers the hard, opinion-heavy UI decision until you actually need shared components.

The verified shift you must not miss

react-native-web has entered maintenance-only mode. Its creator, Nicolas Gallagher, and early adopter Zalando have pivoted to React Strict DOM (RSD), and Software Mansion's ecosystem read is "no major features on the horizon." This does not mean existing RN-Web apps are in trouble — they keep working. It means a new universal bet should not be placed on RN-Web's continued evolution. This is why the default points elsewhere for greenfield UI sharing. (Sources: Gallagher's "One React for web and native"; Callstack's "From React Native Web to React Strict DOM.")

The deeper reason RSD is the heir: RN-Web works by mapping React Native primitives onto HTML (native-first, web-as-target), whereas RSD inverts it — you write strict HTML and CSS, a Babel step flattens it to real DOM + static CSS on web and to RN styles on native (web-standards-first). That direction aligns with where the platform is going.

The UI-sharing landscape, and when each wins

Platform-extension files (.web.tsx / .native.tsx) — one import, two implementations resolved by the bundler. Simple, explicit, no framework buy-in; the honest default when a handful of components need to diverge. It scales poorly if most components need it (then you wanted a real cross-platform UI layer).

React Strict DOM (Meta) — the strategic bet for new universal UI: write standards-based HTML/CSS once, render web + native. Cost is an involved, multi-step setup and a younger ecosystem — you're betting on the trajectory, not the maturity.

Expo Universal Components / @expo/ui — a single component API that renders as native SwiftUI (iOS) / Jetpack Compose (Android) / react-dom (web). Wins on platform fidelity (real native controls, not bridged approximations) and pairs naturally with an Expo app, which is why the entry picks it first for new universal UI inside Expo. Since SDK 56 the native APIs are stable; the web half (backed by react-dom or react-native-web) is still experimental and likely to change. The tradeoff is Expo coupling and an unstable web lane.

Tamagui — cross-platform UI kit with an optimizing compiler; a more batteries-included route when you want a component system rather than primitives.

Solito — not a UI layer but a shared-routing bridge across Expo Router + Next.js, so navigation is one mental model across web and native (see RB-E-NAV).

Expo DOM Components (use dom) — render real web components inside a React Native app via a webview wrapper. The pragmatic escape hatch for incremental reuse of existing web UI in a native shell, not a foundation for a whole app.

The strongest counter-argument: share the spec, not the code

In September 2026 Shopify announced it is moving its mobile apps off React Native to Swift and Kotlin. It does not say RN was slow or wrong — "React Native was the right choice for Shopify in 2020." Its reason is that coding agents made building the same feature twice cheap enough that sharing an implementation lost its value; parity is now held by shared specifications, tests and review checkpoints that agents execute. The entry records this as a named competitor, not a reversal: two agent-maintained copies are cheaper but not free ("that cost has not disappeared"), Shopify's route needed greenfield rebuilds and a bespoke checkpoint-gated system (Helix), and its decision weighs no web app — the case where a shared logic package pays twice. An organisation with that machinery has a real alternative; one without it still pays full price for duplicated logic. The agent-workflow half of the thread lives in RB-E-AI-DEVTOOLS.

Tradeoffs and failure modes to name out loud

  • Sharing UI too aggressively — chasing a single component tree across platforms produces lowest-common-denominator UX. Platforms have conventions (back gestures, tab bars, native pickers); the ecosystem trend is to honor them, not paper over them.
  • The abstraction tax — every Platform.select and per-platform branch is coupling. If a "shared" component is mostly branches, it isn't shared; split it.
  • Rendering RN-on-web vs building web properly — RN-Web-style approaches can yield un-idiomatic, heavier web output. RSD's standards-first direction exists precisely to avoid this.
  • Framework lock-in — Expo Universal Components and Solito tie you to Expo/Next; fine if you're already there, a cost if you're not.

How it interacts with the rest of the stack

  • State & data (RB-E-STATE, RB-E-DATA) — these share cleanly and should live in the common package; they're the easy 80%.
  • Styling (RB-E-STYLING) — RSD is both a code-sharing and a styling strategy (it compiles to static CSS on web); the two entries describe the same tool from two angles.
  • Navigation / meta-frameworks (RB-E-NAV, RB-E-META-FRAMEWORKS) — Solito and Expo Router shape how routing is shared web↔native.
  • Architecture depth — package boundaries, dependency direction, and what may import what are owned by engineering-principles.

In one paragraph

Don't try to "write once, run anywhere"; share by layer. Extract a platform-agnostic logic + state + data package first — the cheap, certain, reversible win. Defer shared UI until you need it, and when you do, bet new work on React Strict DOM or Expo Universal Components, not on react-native-web (now maintenance-only). Only an organisation that can gate agent-maintained duplicate native code has a real alternative: share the spec, not the code. Honor platform UI conventions rather than flattening them. This layered discipline is precisely the duplication react-brain is built to help teams collapse.


See also: RB-E-STATE & RB-E-DATA (the layers that share cleanly), RB-E-STYLING (React Strict DOM as a styling strategy), RB-E-NAV / RB-E-META-FRAMEWORKS (shared routing). Architecture depth: the engineering-principles skill.

Related in app-architecture: state · data · p2p · nav · meta-frameworks · forms · auth · networking · uploads · desktop