Skip to content
·7 min read

Expo Router Is File-Based Routing for React Native (And It's Finally Good)

React Navigation's imperative config is fine for simple apps. Expo Router brings Next.js-style file-based routing to React Native. Here's how to set it up and what to watch for.

ExpoReact NativeRoutingMobile

React Navigation is the routing standard for React Native. It works. It also has a configuration problem. As your app grows, the imperative createStackNavigator and createBottomTabNavigator calls become a tangled mess of strings and parameters. Deep linking is bolted on. Type safety is rare. Refactoring is hard.

Expo Router brings the Next.js model to React Native. Routes are files. Directories are nested layouts. Deep links come for free. Typed routes are an opt-in experiment away. After shipping two client apps on it, I am sold. Here is how to set it up and what to watch for.

The file structure

Every file in app/ is a route. The directory structure is the route hierarchy.

app/
  _layout.tsx          # wraps every screen
  index.tsx            # /
  login.tsx            # /login
  (tabs)/
    _layout.tsx        # tab bar
    index.tsx          # /
    profile.tsx        # /profile
  post/
    [id].tsx           # /post/:id
    index.tsx          # /post

That is the whole routing config. No string-based route names. Add a file, get a route. Delete a file, the route is gone.

Layouts

Each directory can have a _layout.tsx file that wraps every screen inside it. This is where you put tab bars, stack headers, and auth gates.

// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router'
 
export default function TabLayout() {
  return (
    <Tabs screenOptions={{ headerShown: true }}>
      <Tabs.Screen
        name="index"
        options={{ title: 'Home' }}
      />
      <Tabs.Screen
        name="profile"
        options={{ title: 'Profile' }}
      />
    </Tabs>
  )
}

The (tabs) directory name with parentheses is a route group. It groups screens under a shared layout without affecting the URL. The routes inside are still / and /profile, not /tabs/ and /tabs/profile.

Dynamic routes

Bracket notation creates dynamic routes:

// app/post/[id].tsx
import { useLocalSearchParams } from 'expo-router'
 
export default function PostScreen() {
  const { id } = useLocalSearchParams<{ id: string }>()
  // ... fetch post by id
}

/post/123 renders this screen with id === '123'. app/post/[author]/[id].tsx handles /post/jane/123 with both params.

For catch-all routes:

// app/blog/[...slug].tsx
const { slug } = useLocalSearchParams<{ slug: string[] }>()
// slug is ['2026', '08', 'my-post'] for /blog/2026/08/my-post

Typed routes

Turn on typed routes:

// app.json
{
  "expo": {
    "experiments": {
      "typedRoutes": true
    }
  }
}

Expo Router generates types for every route in your app. Navigation calls get autocomplete and compile-time safety:

import { router } from 'expo-router'
 
router.push('/post/123')           // valid
router.push('/post/123/comments')  // valid if that route exists
router.push('/post/123/wrong')     // type error
router.push('/pots/123')           // type error, typo caught

This is huge for refactoring. Rename a route, every link to it breaks at compile time. Add a new route, every screen in the app can navigate to it without writing any config.

Navigation patterns

Three main ways to navigate:

import { router, Link } from 'expo-router'
 
// Imperative
router.push('/post/123')
router.replace('/login')
router.back()
 
// Declarative — for links in JSX
<Link href="/post/123">Read more</Link>
 
// With params
router.push({
  pathname: '/post/[id]',
  params: { id: '123' },
})

The Link component renders a pressable that navigates. It supports asChild to forward props to a child component, which is how you make custom buttons navigate.

Deep linking for free

Expo Router handles deep links by default. Configure the scheme in app.json:

{
  "expo": {
    "scheme": "myapp"
  }
}

Universal Links and App Links work automatically if you set up the association files (see my article on React Native deep linking). The URL /post/123 resolves to app/post/[id].tsx with id === '123'. No manual linking config.

This is the single biggest reason to use Expo Router. Deep linking that "just works" saves weeks of integration work.

Authentication flow

The pattern I use for auth-gated routes:

// app/_layout.tsx
import { Stack, useRouter, useSegments } from 'expo-router'
import { useEffect } from 'react'
 
function useAuthGate() {
  const { session, isLoading } = useSession()
  const segments = useSegments()
  const router = useRouter()
 
  useEffect(() => {
    if (isLoading) return
 
    const inAuthGroup = segments[0] === '(auth)'
 
    if (!session && !inAuthGroup) {
      router.replace('/(auth)/login')
    } else if (session && inAuthGroup) {
      router.replace('/')
    }
  }, [session, isLoading, segments])
}
 
export default function RootLayout() {
  useAuthGate()
 
  return <Stack screenOptions={{ headerShown: false }} />
}

The gate runs on every navigation. If the user is signed out and tries to access a protected route, redirect to login. If they are signed in and on a login screen, redirect home.

Migrating from React Navigation

If you have an existing React Navigation app, you can migrate incrementally:

  1. Keep your existing stack of screens.
  2. Add Expo Router alongside React Navigation using the interop libraries.
  3. Move routes to the file system one at a time.

Or just keep React Navigation. It is not going anywhere. Expo Router is the better default for new projects, but React Navigation is still actively maintained and works fine.

Pitfalls

  • Bundle size — Expo Router adds some KB to your app. Small price for the features, but worth knowing.
  • Build complexity — slightly more setup than vanilla React Navigation. The Expo tooling handles most of it.
  • Learning curve — file-based routing has its own conventions. If your team only knows React Navigation, expect a week of adjustment.
  • Edge cases — modals, presentations, and platform-specific screens have their own patterns. Read the docs once before you start.

When to use Expo Router

Use it for new projects. Use it for projects that need deep linking. Use it for projects where typed navigation matters. Use it for projects where the team has Next.js experience and benefits from the mental model transfer.

Stick with React Navigation if you have an existing app that works and you have no specific reason to migrate.

Expo Router is what React Navigation should have been for file-based routing. Use it for new apps, save yourself the deep linking integration, and enjoy typed navigation.

Want help setting up Expo Router?

I build React Native apps with Expo Router, typed routes, and deep linking baked in from day one. Let's talk.

Frequently Asked Questions

What is Expo Router?

Expo Router is a file-based routing system for React Native, built on top of React Navigation. You define routes as files in the app/ directory, similar to Next.js. It handles nested layouts, dynamic routes, deep links, and typed navigation.

Should I use Expo Router or React Navigation?

For new projects, use Expo Router. The file-based structure scales better, deep linking is automatic, and typed routes catch navigation bugs at compile time. For existing React Navigation apps, migrating is optional — the libraries interoperate.

How do dynamic routes work in Expo Router?

Name a file with brackets to create a dynamic route. app/post/[id].tsx matches /post/123 and exposes id as a parameter via the useLocalSearchParams hook. Splat routes like app/blog/[...slug].tsx match multiple segments.

Does Expo Router support typed routes?

Yes. Set experiments.typedRoutes to true in app.json. Expo Router generates types for every route, and the router.push and router.navigate methods accept only valid paths. This catches typos and dead links at compile time.