If you've spent any time building React Native apps, you've already bumped into the question. You're setting up a new project, you need to move users between screens, and suddenly you're 40 minutes deep in a documentation rabbit hole wondering whether to go with the familiar React Navigation setup or take the leap to Expo Router. This article is going to break it all down, no hype, no fanboy energy, just an honest look at both options so you can make the call that actually fits your project.
First, What Does "Routing" Even Mean in a Mobile App?
On the web, routing is pretty intuitive. You type a URL, the browser goes to a page. Simple. In mobile apps, there's no URL bar, no browser history button, nothing like that. But users still need to move between screens, go back to where they came from, open modals, tab between sections, and all of that has to feel smooth and natural.
So in mobile development, routing is basically the system you use to manage how screens stack on top of each other, how state is preserved when you go back, how deep links work, and how the app knows which screen to show based on where the user is in the flow. Think of it like this: routing is "moving between screens while keeping your app's memory intact." When someone logs in, fills out a form, gets distracted and opens a modal, then comes back, the app should remember all of that. Routing is the machinery underneath that makes it happen.
In React Native, this doesn't come out of the box. The framework gives you the building blocks, but you have to wire up the navigation yourself. That's where libraries come in.
Why Navigation Is Such a Big Deal in React Native
React Native apps are essentially single-page applications. Everything runs in one JavaScript thread, rendered to native views. There's no browser doing the heavy lifting of managing history or transitions. You're responsible for it all.
Bad navigation kills good apps. A janky transition, a broken back button, a modal that doesn't dismiss properly, losing scroll position on a tab switch, these are the things that make users delete an app. Navigation isn't just a developer concern, it's directly tied to how polished your product feels.
On top of that, navigation affects how your code is organized. If your navigation logic is scattered, your codebase gets messy fast. Passing params from screen to screen starts feeling like passing notes in class, chaotic and fragile. The navigation library you choose will shape not just the user experience but the entire architecture of your app.
The Story of React Navigation
React Navigation showed up around 2017 and quickly became the community standard for React Native navigation. Before it, developers were using a library called Navigator that came built into React Native, and honestly, it was rough. It was hard to customize, the API was clunky, and it didn't support a lot of the patterns native apps needed.
React Navigation came in with a JavaScript-based approach, meaning the navigation logic runs entirely in JS rather than relying on native modules. This made it super flexible. You could customize everything. Stack navigators, tab navigators, drawer navigators, modal screens, you could compose all of these together however you needed.
Over the years it matured a lot. React Navigation v5 brought a component-based API that felt much more React-like. v6 improved performance and added better TypeScript support. By the time v7 rolled around, it had become incredibly stable and powerful, with proper native stack support through react-native-screens, smooth gesture handling, and deep link support.
Today React Navigation is still the backbone of a huge portion of React Native apps in production. It's battle-tested, well-documented, and has a massive community around it.
The Problems Developers Were Hitting
Here's the thing though. Even as React Navigation got better, there were real pain points that never fully went away.
The boilerplate was heavy. Every time you added a new screen, you had to register it with the navigator, define the route name as a string (or a typed constant if you were being careful), update your navigation types, and then navigate to it using navigation.navigate('ScreenName'). If you had a large app with lots of screens, this got tedious fast, and it was easy to make typos or let things fall out of sync.
Nested navigators were especially tricky. Imagine a tab navigator inside a stack navigator with a modal stack on top of that. Navigating from a screen deep inside one tab to a screen inside another tab required you to understand the full navigator hierarchy and use navigation.navigate with just the right combination of route names. One wrong step and things broke in subtle ways.
TypeScript support improved a lot but was never painless. You had to manually define types for every route and its params. It worked, but it was ceremony you had to maintain by hand.
Deep linking required a separate configuration object where you mapped URL patterns to route names. So your routing logic was in one place, but your deep link config was somewhere else entirely. Keeping them in sync was a real maintenance burden.
For small apps, none of this is a big deal. But as apps grow, you start to feel it. New developers joining the team had to understand both the navigator structure and the routing types before they could comfortably add a new screen. That's friction.
Enter Expo Router: Why It Was Built
Expo Router launched in 2023 and reached a stable v2 in late 2023, with v3 and v4 following through 2024 and 2025. The idea behind it was borrowed directly from the web. Specifically, from Next.js.
If you've used Next.js, you know how good file-based routing feels. You create a file at pages/about.tsx, and boom, there's a route at /about. No configuration, no registration, just a file. Expo Router brings that same mental model to React Native.
The key insight the Expo team had was this: your file system already knows about your screens. Why make developers maintain a separate navigation config that duplicates that information? Let the file system be the source of truth.
It's also important to understand that Expo Router is not a replacement for React Navigation at the infrastructure level. Under the hood, Expo Router is built on top of React Navigation. It uses the same navigators, the same gesture handling, the same native stack. What it adds is a layer of abstraction that manages the routing config automatically based on your folder structure. So when you use Expo Router, you're not leaving React Navigation behind, you're using a smarter way to configure it.
File-Based Routing Explained Simply
Here's the core concept. In Expo Router, you have an app/ directory. Every file you put in there becomes a screen. The file path becomes the route path.
app/
index.tsx -> / (home screen)
about.tsx -> /about
settings.tsx -> /settings
profile/
index.tsx -> /profile
edit.tsx -> /profile/edit
That's it. No navigator file to update. No route name strings to type. The file structure tells the router everything it needs to know.
Navigating between screens uses the Link component or the router object, and the paths you use match your file structure:
import { Link } from 'expo-router';
<Link href="/profile/edit">Edit Profile</Link>
Or programmatically:
import { router } from 'expo-router';
router.push('/profile/edit');
Because routes are paths (strings derived from your folder structure) rather than arbitrary route names, you get a lot of benefits automatically. TypeScript can infer types from your file structure using Expo Router's typed routes feature. Deep links work out of the box because your routes are already path-based. Sharing links between web and mobile becomes much simpler when both are using the same routing system.
Layouts and Nested Layouts in Expo Router
One of the most powerful things in Expo Router is the layout system. In each folder, you can have a special file called _layout.tsx. This file wraps all the screens inside that folder and lets you define shared UI or navigator configuration.
app/
_layout.tsx <- root layout (wraps everything)
(tabs)/
_layout.tsx <- tab navigator setup
index.tsx <- first tab
explore.tsx <- second tab
(auth)/
_layout.tsx <- auth flow layout
login.tsx
signup.tsx
The root _layout.tsx might set up things like a theme provider, a toast notification system, or your authentication context. The (tabs)/_layout.tsx defines the tab bar and which screens are tabs. The (auth)/_layout.tsx might handle redirecting already-authenticated users away from the login screen.
The parentheses in (tabs) and (auth) are a special Expo Router convention called "route groups." The folder name in parentheses is not included in the URL. It's just organizational. You're grouping screens logically without affecting the route paths.
So (tabs)/index.tsx has the route / not /(tabs)/. This is useful for organizing your code without creating weird nested URLs.
Shared layouts mean that if you're inside the tabs navigator and you navigate between tabs, the tab bar stays put and only the content area re-renders. You get this for free just by structuring your files correctly.
Protected Routes and Auth Flows
Authentication routing is something every real app has to deal with, and it's one of the areas where Expo Router shines with a clean pattern.
The idea is simple. Inside your (auth)/_layout.tsx, you check if the user is logged in. If they are, you redirect them to the main app. If not, you let them through to the login or signup screen. Inside your (tabs)/_layout.tsx or your root layout, you do the opposite. If the user isn't logged in, redirect them to /login.
Here's roughly what that looks like:
// app/(auth)/_layout.tsx
import { Redirect, Stack } from 'expo-router';
import { useAuth } from '@/hooks/useAuth';
export default function AuthLayout() {
const { isLoggedIn } = useAuth();
if (isLoggedIn) {
return <Redirect href="/(tabs)" />;
}
return <Stack />;
}
// app/(tabs)/_layout.tsx
import { Redirect, Tabs } from 'expo-router';
import { useAuth } from '@/hooks/useAuth';
export default function TabsLayout() {
const { isLoggedIn } = useAuth();
if (!isLoggedIn) {
return <Redirect href="/login" />;
}
return <Tabs />;
}
This is clean, explicit, and colocated with the screens it protects. In React Navigation, you'd typically handle this through conditional rendering of different navigator trees or through a more complex navigation state management setup. Both work, but the Expo Router pattern feels more intuitive to reason about.
Performance: Let's Be Honest About What Actually Matters
This is where a lot of comparisons get misleading. People throw around "Expo Router is faster" or "React Navigation has less overhead" without really qualifying what they mean. Let's break it down properly.
Bundle behavior: Expo Router supports automatic code splitting when used with Expo's Metro bundler in web mode. Each route can be lazily loaded, meaning the JavaScript for a screen only loads when the user navigates to it. In native mode (iOS/Android), this matters less because the whole bundle is bundled at build time anyway. React Navigation loads all screens eagerly by default, though you can manually implement lazy loading with the lazy prop on tab navigators.
Navigation transitions: Both approaches produce the same transition quality because Expo Router uses React Navigation's navigators under the hood. The native stack transitions you get from react-native-screens are the same regardless of which tool you use to configure them. There's no meaningful difference here.
Developer workflow speed: This is where Expo Router genuinely wins. Adding a new screen is creating a file. No config update, no type declaration, no route string to maintain. With Expo Router's typed routes, your IDE knows all valid route paths and will autocomplete them. This reduces cognitive load and bugs, which might not be a "performance" metric in the benchmarking sense, but it absolutely speeds up how fast you ship features.
Runtime overhead: Expo Router adds a thin abstraction layer on top of React Navigation. In practice, this overhead is negligible for any real-world app. You're not going to notice it.
Developer Experience: The Real Comparison
If you're a beginner just getting into React Native, Expo Router is genuinely more approachable. The mental model is simpler. Create a file, it's a screen. Navigate using a path. The concept maps directly to things you might already know from web development. You spend less time learning the framework and more time building the actual app.
For teams, file-based routing scales really nicely. When a new developer joins, they can understand the app's screen structure just by looking at the folder tree. You don't need to read a navigator config file to figure out what screens exist. This is a real benefit as teams grow.
For TypeScript users, Expo Router's typed routes (which you enable with "typedRoutes": true in your Expo config) give you end-to-end type safety on your paths. router.push('/profle/edit') will show a type error if you typo the path. That alone has saved me from embarrassing bugs.
React Navigation is not bad at developer experience, not at all. It's very explicit, which has its own value. You always know exactly how your navigators are composed because you wrote every line of it. For developers who prefer explicit configuration over convention, this feels more comfortable. There's also a huge amount of community knowledge, Stack Overflow answers, blog posts, and Discord help available for React Navigation compared to Expo Router.
Scalability for Large Applications
Let's say you're building something serious. A fintech app, a social platform, a healthcare tool. Something with dozens of screens, multiple user roles, complex flows, and a team of ten engineers working on it simultaneously.
With React Navigation at that scale, you'll likely have a main navigator file that's quite long, or you'll split navigators across multiple files and import them into each other. Neither is bad, but the manual nature of it means there's potential for it to drift and become hard to follow. Strong TypeScript discipline helps, but it's discipline you have to actively maintain.
With Expo Router at that scale, the folder structure does most of the organizational work for you. You can create nested groups, separate flows by folder, add route-specific layouts. The structure of the app is visible from the file system. New engineers can get oriented faster.
That said, Expo Router's conventions can feel constraining for non-standard navigation patterns. If you have a very custom navigation UX, like a multi-step wizard with complex back behavior, or a non-standard transition system, you might find yourself working around Expo Router's conventions rather than with them. React Navigation gives you more raw control in those cases.
Real-World App Folder Structure
Here's what a production-ready Expo Router project might look like:
app/
_layout.tsx <- root (providers, fonts, splash)
+not-found.tsx <- 404/unknown route handler
(auth)/
_layout.tsx <- redirect if already logged in
login.tsx
signup.tsx
forgot-password.tsx
(app)/
_layout.tsx <- redirect if not logged in
(tabs)/
_layout.tsx <- tab bar config
index.tsx <- Home tab
explore.tsx <- Explore tab
inbox.tsx <- Inbox tab
profile.tsx <- Profile tab
post/
[id].tsx <- Dynamic route: /post/123
[id]/comments.tsx <- Nested: /post/123/comments
settings/
index.tsx
notifications.tsx
privacy.tsx
account.tsx
modal/
image-preview.tsx <- Modal screens
share-sheet.tsx
The [id] syntax is for dynamic routes where the segment changes, like a user ID or post ID. router.push('/post/123') would render post/[id].tsx with id = "123" accessible via useLocalSearchParams().
Compare this to the equivalent React Navigation setup. You'd have a navigation/ folder with files like RootNavigator.tsx, AuthStack.tsx, AppTabs.tsx, SettingsStack.tsx, and a types.ts file defining all the route params. It works perfectly well, but it's more code to write and maintain.
What Companies and Teams Are Actually Choosing
In 2026, if you're starting a new project and using Expo (which is now the default recommendation from the React Native team itself), Expo Router is the default choice that comes out of the box. The npx create-expo-app template uses Expo Router. The official Expo documentation is written around Expo Router. New Expo features like server-side rendering, universal links, and the web support all integrate most cleanly with Expo Router.
Larger enterprise teams that have existing React Navigation codebases are generally not migrating. The cost-benefit doesn't make sense for stable production apps. If it works, it works.
Startups and new projects lean toward Expo Router for the DX benefits and because it positions them well for Expo's growing ecosystem.
Teams doing pure bare React Native (without Expo) still predominantly use React Navigation directly because Expo Router is designed for the Expo ecosystem. You can technically use it outside Expo, but it's not the intended use case and the experience gets rougher.
When NOT to Use Expo Router
Expo Router is genuinely not the right choice in some situations, and being honest about this matters.
If your project is bare React Native without Expo tooling, stick with React Navigation. Expo Router has dependencies on the Expo build system and Metro configuration. It can work outside of Expo, but you'll hit friction, and the community support for that setup is thin.
If you have highly custom navigation UX that doesn't map neatly to the file-based paradigm, you might find yourself fighting the conventions. Things like multi-step onboarding flows with complex skip logic, paginated full-screen flows, or deeply custom transition animations are easier to achieve with the direct control React Navigation gives you.
If your team is more comfortable with explicit configuration and dislikes "magic," React Navigation's transparency might be worth more than Expo Router's convenience. Good code is code your team understands and trusts.
If you're maintaining an existing React Navigation codebase that's working well, don't migrate for the sake of it. The grass is not always greener, and migrations introduce risk.
When React Navigation Still Makes More Sense
React Navigation is not a legacy tool. It's actively maintained, has a huge community, and is the right choice in plenty of real scenarios.
When you need very fine-grained control over the navigation state, like manually manipulating the navigation stack or implementing complex reset behaviors, React Navigation's imperative API gives you exactly what you need.
When you're building a library or a reusable component that other React Native apps will consume, you probably don't want an Expo Router dependency in your package. React Navigation is the neutral, universal choice.
When your team has deep React Navigation expertise and the project is time-sensitive, using what you know well beats learning new conventions under deadline pressure.
When you're targeting platforms or environments where Expo's toolchain isn't suitable, React Navigation is the safe choice.
Putting It All Together
Here's the honest summary. Expo Router and React Navigation are not really competitors in the way the title of this article might imply. Expo Router is built on React Navigation. Choosing Expo Router means choosing a better configuration experience layered on top of the same navigation infrastructure.
For most new apps being built in 2026 with the Expo ecosystem, Expo Router is the better default. The file-based mental model is intuitive, the TypeScript integration is excellent, and it makes your codebase easier for teams to navigate (pun intended).
For existing apps, apps outside the Expo ecosystem, or teams that value explicit configuration, React Navigation remains a completely solid choice that's not going anywhere.
The best navigation setup is the one your team understands, your users don't notice, and your codebase doesn't make you dread opening. Use that one.
Hope you liked this blog. If thereโs any mistake or something I can improve, do tell me. You can find me on LinkedIn and X, I post more stuff there.




Top comments (0)