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
option
tradeoff
evidence
React Navigation
RN default; mature; imperative + declarative
ships in 13/34
Expo Router
file-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 it
3.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 note
36.6M/wk · ships in 9/34
TanStack Router / Start
type-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 Router
web, full-framework routing + RSC (see RB-E-META-FRAMEWORKS)
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
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
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.
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.
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.)
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.
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.
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.
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.
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.)
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.
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.