Pavan Rangani

HomeBlogExpo Router: Modern React Native Navigation Guide for 2026

Expo Router: Modern React Native Navigation Guide for 2026

By Pavan Rangani · February 27, 2026 · Web Development

Expo Router: Modern React Native Navigation Guide for 2026

Expo Router: File-Based Navigation for React Native Apps

React Native navigation has always been complicated. React Navigation requires manual route registration, nested navigator configuration, and TypeScript types that don’t stay in sync with your actual screens. Expo Router solves this by bringing file-based routing to React Native — the same pattern that made Next.js navigation effortless. Therefore, this guide covers everything from basic setup to advanced patterns like typed routes, deep linking, preloading, and authentication flows.

Why File-Based Routing Fixes React Native Navigation

In React Navigation, adding a new screen requires three steps: create the component, register it in a navigator, and add TypeScript types for the route params. With Expo Router, you create a file in the app/ directory and it automatically becomes a route. The file path is the URL path. No registration, no manual type definitions, no navigator configuration files.

Moreover, Expo Router provides universal deep linking out of the box. Every screen in your app has a URL that works on iOS, Android, and web. Users can share links to specific screens, push notifications can navigate to any route, and your app handles incoming URLs without any additional configuration. This was possible with React Navigation but required significant setup for each route.

app/
├── _layout.tsx          → Root layout (tab/stack navigator)
├── index.tsx            → / (home screen)
├── settings.tsx         → /settings
├── profile/
│   ├── _layout.tsx      → Nested layout for profile section
│   ├── index.tsx        → /profile
│   └── [id].tsx         → /profile/123 (dynamic route)
├── (tabs)/
│   ├── _layout.tsx      → Tab navigator layout
│   ├── home.tsx         → Tab: Home
│   ├── search.tsx       → Tab: Search
│   └── account.tsx      → Tab: Account
├── (auth)/
│   ├── _layout.tsx      → Auth group layout
│   ├── login.tsx        → /login
│   └── register.tsx     → /register
└── [...missing].tsx     → 404 catch-all route

The file structure above creates a complete navigation system: tab navigation, stack navigation, dynamic routes, authentication flows, and a 404 handler — all from the file system. No createStackNavigator, no NavigationContainer, no route registration. Under the hood, Expo Router still runs on React Navigation, so you inherit its battle-tested gesture handling and native screen transitions while shedding most of the boilerplate.

Expo Router React Native mobile app navigation
File-based routing eliminates manual route registration — create a file, get a route

Layouts: Stack, Tabs, and Drawers

Expo Router uses _layout.tsx files to define how child routes are rendered. Each layout wraps its child screens with a navigator — Stack, Tabs, or Drawer. This replaces React Navigation’s nested navigator pattern with a cleaner, co-located approach. Because layouts compose by directory, the navigator hierarchy mirrors your folder hierarchy exactly.

// app/_layout.tsx — Root layout with Stack navigator
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Screen name="(tabs)" />
      <Stack.Screen name="(auth)" />
      <Stack.Screen
        name="modal"
        options={{ presentation: 'modal' }}
      />
    </Stack>
  );
}

// app/(tabs)/_layout.tsx — Tab navigator
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';

export default function TabLayout() {
  return (
    <Tabs screenOptions={{
      tabBarActiveTintColor: '#007AFF',
      headerShown: true
    }}>
      <Tabs.Screen
        name="home"
        options={{
          title: 'Home',
          tabBarIcon: ({ color, size }) =>
            <Ionicons name="home" size={size} color={color} />
        }}
      />
      <Tabs.Screen
        name="search"
        options={{
          title: 'Search',
          tabBarIcon: ({ color, size }) =>
            <Ionicons name="search" size={size} color={color} />
        }}
      />
      <Tabs.Screen
        name="account"
        options={{
          title: 'Account',
          tabBarIcon: ({ color, size }) =>
            <Ionicons name="person" size={size} color={color} />
        }}
      />
    </Tabs>
  );
}

Route groups (parentheses folders like (tabs) and (auth)) organize routes without affecting the URL. The (tabs) group creates tab navigation but the URLs are /home, /search, /account — not /tabs/home. This separation of navigation structure from URL structure is one of Expo Router’s most powerful features. A drawer is just as simple: swap <Tabs> for <Drawer> from expo-router/drawer, and every file in that folder becomes a drawer item.

Typed Routes and Compile-Time Safety

Expo Router generates TypeScript types automatically from your file structure. When you navigate to a route, TypeScript validates both the route path and the required parameters. Consequently, typos in route names and missing parameters become compile-time errors instead of runtime crashes. This is the single biggest reliability win over React Navigation, where route names were plain strings that drifted out of sync silently.

// app/profile/[id].tsx — Dynamic route with typed params
import { useLocalSearchParams, Link, router } from 'expo-router';

export default function ProfileScreen() {
  // TypeScript knows 'id' exists because the file is [id].tsx
  const { id } = useLocalSearchParams<{ id: string }>();

  return (
    <View>
      <Text>Profile: {id}</Text>

      {/* Type-safe Link — IDE autocompletes route paths */}
      <Link href="/settings">Settings</Link>

      {/* Dynamic route with params */}
      <Link href={`/profile/${userId}`}>View Profile</Link>

      {/* Programmatic navigation */}
      <Pressable onPress={() => {
        router.push({
          pathname: '/profile/[id]',
          params: { id: '456' }
        });
      }}>
        <Text>Navigate</Text>
      </Pressable>

      {/* Replace current screen (no back button) */}
      <Pressable onPress={() => router.replace('/login')}>
        <Text>Logout</Text>
      </Pressable>
    </View>
  );
}

// Enable typed routes in app.json
// { "expo": { "experiments": { "typedRoutes": true } } }

Additionally, useLocalSearchParams returns values that persist across re-renders, unlike useGlobalSearchParams which re-renders on every navigation event anywhere in the app. This distinction matters for performance — use useLocalSearchParams for screen-level params and reach for the global variant only when a screen genuinely needs to react to navigation happening elsewhere. A common bug is reaching for the global hook by habit and triggering re-renders on every tab switch.

Mobile app typed navigation and deep linking
Typed routes catch navigation errors at compile time — not runtime

Deep Linking, Prefetching, and the Web Target

Because every route already has a URL, deep linking needs almost no extra work. You register a scheme in app.json (for example myapp://) plus your universal-link domains, and incoming links resolve against the same file tree that powers in-app navigation. Push notifications can carry a url field that you hand directly to router.push, so a notification tap lands the user on /orders/8842 with no switch statement mapping notification types to screens.

Expo Router also supports prefetching to make transitions feel instant. Adding prefetch to a <Link> warms the target route ahead of the tap, which is especially noticeable on the web target where Expo Router renders real, SEO-friendly URLs. Teams typically use this for list-to-detail flows where the next screen is highly predictable.

// Prefetch the detail screen when the row scrolls into view
import { Link } from 'expo-router';

function OrderRow({ order }) {
  return (
    <Link
      href={{ pathname: '/orders/[id]', params: { id: order.id } }}
      prefetch
      asChild
    >
      <Pressable>
        <Text>{order.summary}</Text>
      </Pressable>
    </Link>
  );
}

Authentication Flows and Route Protection

Protecting routes in Expo Router uses a redirect pattern in the root layout. Instead of conditional navigator rendering (the React Navigation approach), you check authentication state and redirect unauthenticated users to the login screen. Newer Expo Router versions also ship a declarative <Stack.Protected guard={...}> API, but the imperative redirect remains the most portable and explicit pattern.

// app/_layout.tsx — Auth-protected root layout
import { Slot, useRouter, useSegments } from 'expo-router';
import { useAuth } from '../contexts/AuthContext';
import { useEffect } from 'react';

export default function RootLayout() {
  const { user, isLoading } = useAuth();
  const router = useRouter();
  const segments = useSegments();

  useEffect(() => {
    if (isLoading) return;

    const inAuthGroup = segments[0] === '(auth)';

    if (!user && !inAuthGroup) {
      router.replace('/login');        // Not logged in
    } else if (user && inAuthGroup) {
      router.replace('/home');         // Already logged in
    }
  }, [user, isLoading, segments]);

  if (isLoading) return <LoadingScreen />;

  return <Slot />;
}

This pattern is simpler and more predictable than conditionally rendering different navigators. The navigation structure is always the same — the layout just redirects based on auth state. Furthermore, deep links work correctly: if a logged-out user opens a link to /profile/123, they get redirected to login and can be sent back afterward. One edge case to watch: redirecting before the navigation tree has mounted throws an error, which is exactly why the guard runs inside useEffect after the loading check rather than during render.

When NOT to Use Expo Router (Trade-offs)

File-based routing is not free of friction. Because routes are derived from the filesystem, very dynamic or programmatically generated navigation graphs feel awkward — you sometimes end up creating placeholder files just to satisfy the router. Large apps can also accumulate deeply nested folders where the URL-to-file mapping becomes hard to scan, and refactoring a route means moving files rather than editing a config object, which can confuse Git history.

Additionally, Expo Router is tightly coupled to the Expo toolchain and its config plugins. If your project is a bare React Native app with heavy native customization that resists Expo’s prebuild model, adopting it can mean fighting the framework. Finally, teams with a large, stable React Navigation codebase and no need for web URLs or deep-linking polish may simply not see enough upside to justify a migration. In those cases, staying on React Navigation is a perfectly defensible choice.

Migrating from React Navigation

Migration from React Navigation to Expo Router can be done incrementally. Start by converting your app entry point and root navigator, then migrate screens one at a time. The key mapping: createStackNavigator becomes a _layout.tsx with <Stack>, createBottomTabNavigator becomes a (tabs)/_layout.tsx with <Tabs>, and navigation.navigate('Screen') becomes router.push('/screen').

Most React Navigation screen options translate directly — headerTitle, headerStyle, tabBarIcon, and presentation all work the same way. However, custom navigators (like a drawer with bespoke content) require more adaptation. The Expo docs publish a migration guide with detailed mapping for every React Navigation API, and because Expo Router sits on top of React Navigation, both can coexist during a phased rollout.

React Native app development and migration
Migrate from React Navigation incrementally — one navigator at a time

Related Reading:

Resources:

In conclusion, Expo Router brings the simplicity of file-based routing to React Native and finally makes React Native navigation feel coherent. It eliminates boilerplate navigator configuration, provides automatic deep linking, generates TypeScript types from your file structure, and simplifies authentication flows. If you’re starting a new project or modernizing an existing one — and you can live within the Expo toolchain — Expo Router is the recommended navigation solution.

← Back to all articles