Ditching the Hand-Wired Navigator: What Expo Router Actually Buys You
Expo Router replaces hand-wired React Navigation stacks with file-based routing. Here's a concrete three-screen migration, what typed routes catch, and where the convention fights back.
01 Oct 2026, 06:33 UTC

If you've built a React Native app with plain React Navigation, you know the drill: every new screen means touching a central navigator file, registering the route, wiring up params types by hand, and hoping nobody renames anything. By screen fifteen, that navigator file is the part of the codebase everyone is afraid to touch.
Expo Router replaces that central registry with a convention: files in an app/ directory are your routes. It's the same idea Next.js popularized on the web, brought to React Native — and it's built on top of React Navigation, not instead of it. That last point matters more than it sounds, and I'll come back to it.
The concrete problem: one navigator file, many regrets
A typical hand-wired setup looks like a Stack.Navigator with three screens, each with a typed params list, each imported and registered manually. Adding a screen means editing that file; removing one means remembering to clean it up. Deep links need a separate linking config object that duplicates your route structure in a second place, so the two drift apart over time.
Expo Router's thesis is that the file tree is a better source of truth. You get one canonical structure that drives navigation, deep links, and (if you opt in) TypeScript checking of your links.
A worked example: three screens, zero navigator boilerplate
Assume a current Expo SDK project created with the default template (which includes Expo Router; check expo-router's version against your SDK's compatibility table before copying any of this — the API has shifted across SDK releases).
Instead of a Stack.Navigator, you create three files:
app/
_layout.tsx // shared navigator for this directory
index.tsx // the "/" screen
details/
[id].tsx // the "/details/:id" screenapp/_layout.tsx declares the navigator for everything in its directory:
import { Stack } from 'expo-router';
export default function RootLayout() {
return <Stack />;
}The dynamic segment [id].tsx reads its parameter with a hook:
import { useLocalSearchParams } from 'expo-router';
import { Text, View } from 'react-native';
export default function DetailsScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
return (
<View>
<Text>Details for item {id}</Text>
</View>
);
}Navigating is a string, not a screen name you had to register:
import { router } from 'expo-router';
router.push(`/details/${item.id}`);Because the route is a path, deep linking falls out of the same structure — yourscheme://details/42 maps to the same screen with no parallel linking config to maintain. (The scheme itself still has to be configured in your app config, and on bare/production builds you still need the platform pieces: Android intent filters, iOS associated domains. The router doesn't magic those away.)
To verify the migration worked: run the app in Expo Go on a simulator and on a physical device, navigate from index to a details screen, then open a deep link directly to /details/42 from a cold start. Both paths should land on the same screen with the same param.
Typed routes: broken links become compile errors
The feature I'd enable first is typed routes. It's opt-in and still flagged experimental in supported SDK versions, so treat it as something to verify against your SDK's docs rather than assume. When enabled, Expo Router generates TypeScript types for valid href strings, so router.push('/detials/42') fails at build time instead of doing nothing on a user's phone.
The check is simple: enable it per your SDK's instructions, deliberately pass an invalid href, and confirm tsc (or your editor) flags it. One caveat: generated types can lag behind route changes until regenerated, so if a valid route suddenly fails type-checking, regenerate before assuming your code is wrong.
The trade-offs worth knowing before you commit
Convention is a constraint. File-based routing assumes your navigation graph maps cleanly onto a directory tree. Apps with highly dynamic, permission-driven navigation — where the entire graph changes based on feature flags or roles — can end up fighting the convention with redirect logic scattered across layouts.
The escape hatch is real but has a cost. Because Expo Router sits on React Navigation, you can drop down to the underlying APIs for custom transitions or gestures the file convention doesn't expose. But once parts of your app use the convention and parts bypass it, you've reintroduced the two-sources-of-truth problem, just smaller.
Web parity is a claim to test, not assume. The same route structure can serve web via Expo's web support, and that's a genuine differentiator — but each screen's components need web-compatibility testing before you promise stakeholders one codebase, three platforms.
Where to start
Don't migrate an existing app wholesale. Create a fresh project with the current SDK template, rebuild one small flow — a list screen and a details screen — and run it on a simulator and a physical device via Expo Go. Then test one deep link end-to-end in a development build, since Expo Go can mask scheme-configuration issues. If that flow is noticeably less code and less coordination than your current setup, you have your answer. If you immediately need the React Navigation escape hatch for basic navigation, you also have your answer — just the other one.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.