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

entriesapp-architecture · verified 2026-08-18 · react + react-native

Navigation & routing

reviewedconfidence: mediumtrack: heldthis tier holds 67% on the public scorecard (6/19 graded · 0 overturned) →

related decisions: meta-frameworks

cited by: meta-frameworks · sheets

re-verified 5× — 2026-08-18 · 2026-07-28 · 2026-07-20 · 2026-07-16 · 2026-07-13 · changelog

recommendation

React Native → Expo Router if you're on Expo, else React Navigation. Web → use the meta-framework's router (see RB-E-META-FRAMEWORKS), or TanStack Router for a type-safe SPA.

  • Expo app → Expo Router (file-based)
  • bare RN → React Navigation
  • type-safe web SPA without a meta-framework → TanStack Router
  • web SPA already on React Router, or ecosystem/familiarity over route type-safety → React Router 8 (released, ESM-only; its typegen is framework-mode-only — which is why TanStack keeps the type-safe-SPA slot)
  • adopting React Navigation 8 native tabs → still ALPHA in 2026; pin + verify against the RN8 release notes first
  • links must survive a store install (invites, referrals, shared products) → deferred deep linking, i.e. an attribution SDK or Detour; plain Universal/App Links lose the intent at the store boundary (see reading)

Options & tradeoffs

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

optiontradeoffevidence
React NavigationRN default; mature; imperative + declarativeships in 13/34
Expo Routerfile-based; great if on Expo; convention over config; v56 (SDK 56) DECOUPLED from React Navigation — its own native-first core; direct @react-navigation/* imports no longer work alongside it3.8M/wk · ships in 1/34
React Router (8.x)web routing; absorbed Remix; v8 (8.3.0, 2026-07, verified vs npm) RELEASED — ESM-only (drops CJS + react-router-dom), future flags on by default incl. middleware (v8_middleware flag removed — auth/logging/headers now first-class via RouterContextProvider); v7→v8 generally non-breaking if you're caught up on v7 future flags. SECURITY FLOOR: 8.3.0 / 7.18.0 — see note36.6M/wk · ships in 9/34
TanStack Router / Starttype-safe web routing; signal-based core; Start is its RSC/SSR meta-framework (see RB-E-META-FRAMEWORKS)19.7M/wk · ships in 2/34
Next.js App Routerweb, full-framework routing + RSC (see RB-E-META-FRAMEWORKS)

evidence: npm weekly downloads (signals snapshot) · “ships in n/D” = adoption across the production-app census, honest denominators

Migration lines

version and deprecation lines this decision tracks — for YOUR repo's sequenced plan: the doctor or npx -y @heart-it/react-brain migrate .

react-router-dom → react-router v8 (single package — v8 drops react-router-dom) [superseded · effort M]

v8 (8.x, released 2026) is ESM-only and drops both CJS and the react-router-dom package; v7→v8 is generally non-breaking if you're caught up on v7 future flags

reactrouter.com

react-router < 8.3.0 → react-router 8.3.0 (or 7.18.0 if you must stay on v7) [upgrade · effort S]

SECURITY: six advisories published 2026-07-22 — unauthenticated __manifest DoS (High, CVSS 7.6), RSC-mode CSRF bypass, RSCErrorHandler XSS, SSR-hydration constructor injection, two open redirects — fixed in 8.3.0 and backported to 7.18.0. Moving all the way to v8 also gets ESM-only + future flags on by default, generally non-breaking from a flags-current v7

github.com/remix-run/react-router/security/ad… · github.com/remix-run/react-router/security/ad… · registry.npmjs.org/react-router/latest

npm weekly downloads (from the corpus's last signals run): expo-router 3.8M · react-router-dom 36.6M · react-router 44.2M · @tanstack/react-router 19.7M

Verified notes

React Navigation 8.0 (ALPHA, announced 2025-12) makes NATIVE bottom tabs the default (via react-native-screens — native look incl. iOS 26 liquid glass), adds native icon components (SFSymbol on iOS / MaterialSymbol on Android — smaller bundles), reworks TypeScript inference, and adds screen-history APIs (pushParams, persistor, inactiveBehavior via React.Activity). Reflects the trend toward honouring platform UI. Still ALPHA in 2026 — pin/verify before adopting. Verified vs the React Navigation blog. RN8 JULY PROGRESS (2026-07-08, verified vs the React Navigation blog): still PRE-STABLE (next tag) — adds Suspense-enabled navigation (interruptible, concurrent), experimental UNSTABLE_loader data loaders (TanStack Query/Apollo prefetch), full parent-navigator type inference (drops CompositeNavigationProp), and a cross-library STANDARD NAVIGATION API so custom navigators work in both React Navigation AND Expo Router — the first bridge across the newly-decoupled stacks. Requires screens 4.25+, gesture-handler 3.0+, TS 6+. UPDATE 2026-07-09: Expo Router v56 (SDK 56) NO LONGER DEPENDS on React Navigation — "expo-router no longer depends on react-navigation"; code importing @react-navigation/* directly breaks when adopting it (verified vs the SDK 56 changelog; a codemod exists — npx expo-codemod sdk-56-expo-router-react-navigation-replace). The decoupling persists in SDK 57 (expo-router 57.x ships zero @react-navigation/* deps, verified vs npm 2026-07-10). The Expo-vs-bare split above now separates two DISTINCT navigation stacks, not one wrapper over the other — which raises the cost of switching lanes later; weigh it at project setup. Stress-tested 2026-07-10 (WEAKENED → React Router when-clause added; the TanStack type-safe-SPA slot SURVIVED the 2.3x-downloads attack — RR's typegen is framework-mode-only). RE-CHALLENGED 2026-08-18 → HELD: same attack, bigger gap (react-router 44.2M/wk vs @tanstack/react-router 19.7M/wk, 2.2×), and the axis is unchanged at the source — React Router 8.3.0's type generation "executes your route config (app/routes.ts by default)" and emits per-route-module +types, i.e. it is tied to FRAMEWORK-mode conventions, so a declarative/data-mode SPA still does not get it (verified vs reactrouter.com/explanation/type-safety). Popularity moved; the reason did not. UPDATE 2026-07-16 (TWiR #290, verified vs the release): react-native-screens 4.26 marks the Tabs API STABLE (requires RN 0.84+; FormSheet + Stack-v5 toolbar menus stay experimental) — the native layer under RN8's native bottom tabs firms up while RN8 itself remains pre-stable. REACT ROUTER SECURITY FLOOR (2026-07-28, verified vs the GitHub advisories): six advisories published 2026-07-22 against react-router — including an unauthenticated DoS on the __manifest endpoint (High, CVSS 7.6, affecting >=7.0.0 <7.18.0), an RSC-mode CSRF bypass that executes the action before the 400 response, an RSCErrorHandler protocol-validation XSS, arbitrary client-side constructor injection via SSR hydration, and two open-redirect variants (one a CVE-2025-68470 bypass). Fixed in 8.3.0 and backported to 7.18.0 — the team advises upgrading to one of those. Note the pattern: half of these live in the RSC/SSR surface, so the exposure scales with how much of React Router's server side you actually use.

Canonical reading

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

Detour: deferred deep linking done right in Expo RouterBartek Krasoń (Software Mansion), guest post on the Expo blog

Names a hole every referral/invite flow eventually falls into: standard deep links (Universal Links / App Links) only work when the app is ALREADY installed — iOS and Android preserve nothing across the store boundary, so a new user who taps an invite link launches the app blind to what they clicked. That is the Installation Gap, and closing it needs a server that remembers the click, which is why this has historically meant adopting an attribution SDK (deep linking as its side feature) and hand-writing glue between its callback and your router. The router-side argument is the durable part: because Expo Router exposes `+native-intent.tsx` as pre-routing middleware (redirectSystemPath), the deferred link can be resolved BEFORE the first screen mounts, which removes the post-mount useEffect race, duplicate navigation events and the ordering problem with auth gates; the remaining case — link resolves, user isn't signed in yet, guard redirects to /login and swallows it — is handled by holding the intent in a provider until the app says the user is ready. Vendor content (SWM's own SDK, @swmansion/react-native-detour 2.3.1, and it needs a Detour account for app credentials), but the failure mode and the +native-intent hook are general.

Reverse Engineering ChatGPT Web: How OpenAI Built for a Billion UsersDennis Brotzky (performance.dev)

Production evidence for React Router 7 framework mode at billion-user scale: ChatGPT migrated Next.js Pages Router → Remix (2024) → RR7 with streaming SSR, TanStack Query seeded from server render, Tailwind 4 tokens, ProseMirror composer — 84KB HTML document, 50-65ms TTFB. The concrete counterweight to 'serious apps need Next.js', and the highest-profile data point behind this entry's React Router when-clause. (4 independent newsletter signals in one week.)

TanStack Router's New Reactive Core: A Signal GraphFlorian Pellet (TanStack)

The primary source behind this entry's 'signal-based core' claim: the monolithic router.state was decomposed into a graph of independent stores so route changes update only affected subscribers (React navigation 7ms→4.5ms in their measurements). The architecture under TanStack Router's fine-grained reactivity.

Exploring Inlined Requires: does Expo Router give you screen-level lazy loading?Andrei Calazans

Instrumented proof that Expo Router defers screen loading by construction via require.context() getters wrapped in getComponent thunks — with the precise caveats (layouts/initial route load eagerly; sync require, not bundle-splitting). The canonical explanation of Expo Router's lazy-loading mechanism.

React Router loaders and actions as integration pointsSergio Xalambrí (React Router / Remix)

Frames loaders/actions as the HTTP-to-domain seam — keep business logic in testable services, reserve loaders/actions for the integration layer (E2E). A durable testing/architecture philosophy for React Router.

Deep Links With Authentication in React NavigationSatyajit Sahoo (Callstack; React Navigation maintainer)

The redirect-after-login problem: a deep link that lands on the login screen should still deliver the user to the intended screen afterwards. Walks the manual approaches in older React Navigation and the new API that handles it (routeNamesChangeBehavior 'lastUnhandled' re-dispatching the unhandled link when auth state flips). From the library's own maintainer.

Migrating to native stack navigation, with a surprise from iOS 26Jose Blanco (thoughtbot)

The JS-stack-vs-native-stack behavioral difference no other reading here covers, learned in a real 0.77→New-Arch migration: moving to native-stack means the REAL platform components apply — the app suddenly inherited iOS 26 Liquid Glass styling (opt out with UIDesignRequiresCompatibility in Info.plist while unready), and a 55pt header logo got clipped because the native bar is a hard 44pt. The durable frame: JS-stack headers merely TOLERATED overflow the platform never allowed; native-stack enforces what iOS actually permits, so audit header content heights when migrating. (Live URL is bot-walled — content verified via Wayback snapshot 2026-07-15.)

Untangling Dialogs in React RouterDave Adams

Models modal dialogs as nested routes in React Router 7 instead of local useState/useEffect — covering revalidation, view transitions, flash sessions, and exit animations. A durable routing pattern that generalizes beyond the example.

Sources

The full explanation

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

About navigation & routing in React & React Native

Diataxis: Explanation. This page builds understanding of the navigation/routing decision — the reasoning behind the pick. It is not a tutorial: the candidate list and one-line tradeoffs live in the index entry RB-E-NAV; framework-level routing (SSR/RSC) is owned by RB-E-META-FRAMEWORKS. Read this to understand why.

The one distinction that organises everything

"Navigation" hides two different problems that share a word but not a nature:

  • Native navigation (React Native) is a UI concern. The thing you choose is how screens stack, how tabs and drawers behave, and how the platform's own transitions and gestures (the iOS back-swipe, native bottom tabs, the Android predictive back) feel. There is no URL; there is a navigation state tree.
  • Web routing (React DOM) is a URL concern, and increasingly a data/server concern. The route is the address bar, history, deep links, and — once a meta-framework is involved — where data loads and where the server boundary sits.

So the first question is never "which router?" It is "which world am I in, and on the web, do I already have a meta-framework?" Everything else follows.

The default, and why

React Native: Expo Router if you're on Expo, otherwise React Navigation. Web: use the meta-framework's router (RB-E-META-FRAMEWORKS); for a type-safe SPA with no meta-framework, TanStack Router.

The RN default is about fit, not superiority. React Navigation is the mature, bare-RN baseline — imperative and declarative, deeply customizable. Expo Router layers file-based routing on top of it: if your project is already Expo, convention-over-config removes a class of boilerplate and gives you deep-linking and universal (web) routing close to free. The choice is "are you on Expo?", not "which is better."

On the web, routing rarely stands alone anymore. If you've chosen Next.js or TanStack Start, they own routing — picking a separate router would fight the framework. The standalone choice (TanStack Router) earns its place precisely when you want a client-rendered SPA with first-class type-safe routes and don't want a server framework.

The landscape, and when each one wins

React Navigation — the bare-RN default. Wins on maturity and control. The 2026 trend it embodies is honouring the platform: React Navigation 8.0 (still alpha through 2026) makes native bottom tabs the default and adds native icon components (SFSymbol on iOS, Material Symbols on Android). That direction matters more than the version — but because 8.0 is alpha, pin and verify before adopting its native tabs.

Expo Router — file-based routing for Expo apps, and the seam through which an Expo app also gets web routing. It's the right default the moment you're on Expo; its cost is Expo coupling. (It is also the RN/universal meta-framework — see RB-E-META-FRAMEWORKS.)

React Router (8.x) — the web workhorse that absorbed Remix. v8 is released, ESM-only (dropped CommonJS and the react-router-dom split), with future flags on by default; v7→v8 is largely non-breaking if you kept up with v7's flags. Use it for web routing when you want a router (or framework mode) without committing to Next.

TanStack Router / Start — type-safe routing with a signal-based core. Router is the SPA story (the strongest type-safety in the field); Start is its SSR/RSC meta-framework (RB-E-META-FRAMEWORKS). Reach for Router when end-to-end route type-safety is the thing you care about.

Next.js App Router — not a router you "add" but the routing of a full framework (RB-E-META-FRAMEWORKS); listed here only so the boundary is clear.

Tradeoffs and failure modes to name out loud

  • Adopting alpha as if it were stable. React Navigation 8's native tabs are compelling but alpha in 2026; shipping on them without pinning is a self-inflicted upgrade treadmill.
  • Fighting your meta-framework's router. Bolting a second router onto Next/Start is the classic web mistake — the framework already owns the URL, data, and server boundary.
  • Flattening platform navigation. A single shared navigation abstraction across web and native tends to produce non-native transitions and gesture handling. Navigation is the layer where "share everything" hurts most (see RB-E-CROSSPLATFORM); share the screens' logic, not necessarily the navigator.
  • Treating routing as pure UI on the web. On the web the URL is also application state and a data trigger; modelling it as local component state (modals that should be routes, filters that should be search params) is a recurring smell.

How it interacts with the rest of the stack

  • Meta-frameworks (RB-E-META-FRAMEWORKS). On the web this is the dominant fork: framework-owned routing vs a standalone SPA router. This page is "which navigator"; that page is "do you have a server framework at all."
  • Cross-platform (RB-E-CROSSPLATFORM). Solito and Expo Router shape how routing is shared web↔native; the honest default is to share screens and logic, not force one navigator across both worlds.
  • State (RB-E-STATE). The URL is server-adjacent state on the web (search params, route params); keep it in the router, not duplicated in a client store.

In one paragraph

Decide the world before the router. On React Native, navigation is native UI: Expo Router if you're on Expo, else React Navigation — and honour platform tabs/gestures rather than flattening them (React Navigation 8's native tabs are the trend, but pin them while they're alpha). On the web, routing belongs to your meta-framework if you have one; if you don't and you want a type-safe SPA, reach for TanStack Router, or React Router 8 for a framework-agnostic router. The recurring mistake is treating these two worlds as one.


See also: RB-E-META-FRAMEWORKS (server-owned routing + SSR/RSC), RB-E-CROSSPLATFORM (sharing navigation web↔native), RB-E-STATE (the URL as state).

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