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 (at ≥8.3.0; a v7 app needs ≥7.18.2; its typegen is framework-mode-only, which is why TanStack Router keeps the type-safe-SPA slot)
adopting React Navigation 8 native tabs → pin the exact 8.0 alpha (still alpha as of 8.0.0-alpha.48, 2026-09-22; verify against the RN8 release notes)
links must survive a store install (invites, referrals, shared products) → deferred deep linking (an attribution SDK or Detour; plain Universal/App Links lose the intent at the store boundary, see reading 'Detour: deferred deep linking done right in Expo Router')
Options & tradeoffs
the field considered — and why each one isn’t the default here
option
tradeoff
evidence
React Navigation
The bare-RN default: mature, imperative + declarative, deeply customizable. 7.4.1 is latest; 8.0 is still alpha (8.0.0-alpha.48 on the next tag, 2026-09-22, verified vs npm). See note: REACT NAVIGATION 8
ships in 13/34
Expo Router
File-based, convention over config; the pick on Expo. Since v56 (SDK 56) its own native-first core, decoupled from React Navigation: direct @react-navigation/* imports no longer work alongside it. SDK 58 (beta) graduates data loaders, SSR, middleware and native tabs out of experimental. See note: EXPO ROUTER
8.4M/wk · ships in 1/34
React Router (8.x)
Web routing; absorbed Remix. v8 is ESM-only, drops react-router-dom and turns future flags (incl. middleware) on by default; 8.4.0 (2026-09-15, verified vs npm). SECURITY FLOOR: 8.3.0 on v8, 7.18.2 on v7. See note: REACT ROUTER SECURITY
55.1M/wk · ships in 9/34
TanStack Router / Start
Type-safe web routing on a signal-based core; Start is its RSC/SSR meta-framework (see RB-E-META-FRAMEWORKS).
25.1M/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 8.4M · react-router-dom 55.1M · react-router 68.7M · @tanstack/react-router 25.1M
Verified notes
Navigation is two problems. On React Native it is native UI: a navigation state tree, platform transitions and gestures, no URL. On the web it is the URL, and once a meta-framework is involved, where data loads and where the server boundary sits. On RN the choice is "are you on Expo?", and since SDK 56 that choice separates two distinct stacks (Expo Router's own core vs React Navigation), not a wrapper over one library, so switching lanes later costs more; decide it at project setup. Expo Router's measured cold-start premium on one low-end Android device (see reading 'React Native Navigation Benchmarks') buys deep linking, lazy screens, file-based routing and web support. On the web the meta-framework owns routing; the standalone slot goes to TanStack Router because only it gives route type-safety to a plain SPA.
EXPO ROUTER (2026-09-15, verified vs the Expo SDK 58 beta and SDK 56 changelogs; npm 2026-09-24): v56 made expo-router independent of react-navigation (codemod: npx expo-codemod sdk-56-expo-router-react-navigation-replace), and expo-router 57.x ships zero @react-navigation/* deps. SDK 58 (beta; expo 58.0.0-preview.6 on npm's next tag) makes data loaders, SSR and middleware stable with no experimental flags, and native tabs, toolbars and the standard-navigation integration stable too; a loader can call setResponseHeaders({'Cache-Control': 'private, max-age=60'}) from expo-server to let clients cache its response. The navigation core was reworked so state is deterministic, and it no longer carries most of the forked React Navigation API: most apps won't notice, but a custom navigator or any import from expo-router/react-navigation must follow the migration guide. JS Tabs, TopTabs and Drawer now run through the same standard-navigation integration as NativeTabs. Before upgrading: web async routes are on by default (asyncRoutes: { web: false } opts out); navigation uses React transitions to keep the current screen visible while the next loads (activityEnabled / NavigationAwareActivity hide inactive content while preserving state); per-screen error boundaries (unstable_settings.screenErrorBoundary or <Stack unstable_screenErrorBoundary={...} />) keep the navigator mounted when one screen throws; on Android, expo-symbols (NativeTabs.Trigger.Icon with md) and @expo/ui (Stack.Toolbar) are optional peers. @expo/ui also gained iOS navigation primitives (NavigationStack, NavigationLink, NavigationDestination, Toolbar, navigationTitle, two/three-column NavigationSplitView): not a router, but a SwiftUI-native navigation surface for iPad/split-view layouts (RB-E-NATIVE-UI owns it).
REACT NAVIGATION 8 (verified vs the React Navigation blog 2025-12-19 and 2026-07-08; npm 2026-09-24): still alpha, 8.0.0-alpha.48 on the next tag while latest is 7.4.1. It makes native bottom tabs the default (via react-native-screens, including iOS 26 liquid glass), adds native icon components (SFSymbol / MaterialSymbol), reworks TypeScript inference with full parent-navigator inference (drops CompositeNavigationProp), adds screen-history APIs (pushParams, persistor, inactiveBehavior via React.Activity), Suspense-enabled navigation, data loaders for TanStack Query/Apollo prefetch (UNSTABLE_loader), and a cross-library standard navigation API so custom navigators work in both React Navigation and Expo Router. Requires react-native-screens 4.25+, gesture-handler 3.0+, TS 6+. Underneath, react-native-screens 4.26 (2026-07-16, verified vs the release) marked its Tabs API stable (RN 0.84+; FormSheet and Stack-v5 toolbar menus are not yet).
REACT ROUTER 8 (verified vs remix.run/blog/react-router-v8 and npm): 8.0.0 shipped 2026-06-17; 8.4.0 (2026-09-15) is current. ESM-only with ES2022 targets; minimums Node 22.22.0+, React 19.2.7+, Vite 7+; react-router-dom removed; the v8_* future flags (including middleware, now first-class via RouterContextProvider) are the default. From a flags-current v7 the upgrade is generally non-breaking.
ROUTE TYPE-SAFETY (2026-08-18, verified vs reactrouter.com/explanation/type-safety): React Router's type generation executes the route config (app/routes.ts) and emits per-route-module +types, so it is tied to framework mode; a declarative or data-mode SPA does not get it. Downloads favor React Router (44.2M/wk vs 19.7M/wk for @tanstack/react-router, 2.2x, 2026-08-18) but the axis that decides the SPA slot is type-safety, and it has not moved.
REACT ROUTER SECURITY (2026-09-24, verified vs the GitHub advisories): six advisories published 2026-07-22. Fixed in 7.18.0: unauthenticated __manifest DoS (CVE-2026-55685, High, CVSS 7.6, >=7.0.0 <7.18.0), RSCErrorHandler protocol-validation XSS (CVE-2026-53667), SSR-hydration constructor injection (CVE-2026-53666, from 6.4.0) and an external redirect via untrusted paths that bypasses CVE-2025-68470 (CVE-2026-53669, from 6.0.0). Fixed in 7.18.2 / 8.3.0: the RSC-mode CSRF bypass that executes the action before the 400 response (GHSA-qwww-vcr4-c8h2, >=7.12.0 <7.18.2 and >=8.0.0 <8.3.0). Fixed in 7.13.0 / react-router-dom 6.30.6: an open redirect leading to XSS (CVE-2026-53668). The floor is therefore 8.3.0 on v8 and 7.18.2 on v7; v6 has no patched release for the injection and redirect-bypass advisories, so v6 apps must move to v7 or v8. Half of these live in the RSC/SSR surface, so exposure scales with how much of React Router's server side you use.
RB-E-META-FRAMEWORKS owns framework-level routing (SSR/RSC); RB-E-CROSSPLATFORM owns sharing navigation web↔native; RB-E-NATIVE-UI owns @expo/ui.
HISTORY:
2026-09-24 CORRECTED: the React Router v7 security floor is 7.18.2, not 7.18.0 (the RSC-mode CSRF fix landed in 7.18.2).
2026-08-18 CHALLENGED → SURVIVES: same downloads attack with a 2.2x gap; typegen still framework-mode-only, so the TanStack type-safe-SPA slot held.
2026-07-10 CHALLENGED → WEAKENED: React Router when-clause added; the TanStack type-safe-SPA slot survived the 2.3x-downloads attack.
Canonical reading
Editorial annotations on why each piece matters — the articles themselves are the originals; read them there.
The measured evidence this entry's RN recommendation never had: the same app built four times over four navigation libraries and profiled on one Android device (Expo SDK 56, RN 0.85, Hermes, New Architecture, Samsung Galaxy A16, release/profileable builds; cold start = the OS Displayed metric, median of 3; FPS/CPU/RAM via Flashlight driving Maestro; breakdowns from Perfetto Systrace + source-mapped Hermes CPU profiles, all data and scripts published). Cold start: react-native-navigation 316 ms · React Navigation v7 358 ms · navigation (Graham Mendick) 398 ms · Expo Router 917 ms. Peak RAM: 195 / 214 / 241 / 308 MB. All four hold ~60 FPS, so the difference is startup and memory, not smoothness. The WHY matters more than the ranking: (1) Expo Router's ~3x cold start is NOT Reanimated — a controlled experiment put Reanimated at only ~62 ms — it is a 2x bigger bundle plus router-on-top-of-React-Navigation layering that evaluates 106 JS modules at boot against 36–43 for the others; (2) Reanimated IS the RAM story — adding it alone to the leanest app reproduced Expo Router's entire ~125 MB premium, almost all anonymous heap from the second Hermes runtime Worklets spins up; (3) react-native-navigation wins because its tabs and stack are Kotlin views, so its JS bundle evaluates in 55 ms against React Navigation's 168 ms of real component tree — but the author's own caveat is that the same design chokes on large screens and drops frames. Read the caveats before quoting the table: rn-navigation is a BARE RN app (some of its lead is 'no expo-modules-core', not the navigation library), it is one device and one trivial UI, and Expo Router buys deep linking, lazy screen loading, file-based routing and web support that this benchmark does not price.
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 gives Expo apps
file-based routing: convention-over-config removes a class of boilerplate and gives you
deep-linking and universal (web) routing close to free. Since SDK 56 it runs on its own
native-first core rather than on React Navigation, so the two are separate stacks and
switching later costs more. 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. Security floor: six advisories
published 2026-07-22 (a High __manifest DoS, RSC-mode CSRF, XSS, hydration injection, open
redirects) mean the minimum is 8.3.0 on v8 or 7.18.2 on v7; v6 has no patched release for
some of them, so v6 apps need to move. Its route type generation is framework-mode-only, which
is why TanStack Router keeps the type-safe-SPA slot.
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.
Plain deep links across a store install. Universal/App Links only work when the app is
already installed; invite and referral links that must survive the install need deferred deep
linking (an attribution SDK or Detour) resolved before the first screen mounts.
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.