Expo Router in React Native: File-Based Navigation Complete Guide

Master Expo Router for React Native with this guide covering file-based routing, layouts, dynamic routes, typed navigation, and advanced patterns like modals and tabs.

Expo Router file-based navigation system for React Native mobile applications

Expo Router brings file-based routing to React Native, replacing manual navigation configuration with a convention-driven approach inspired by Next.js. Starting with Expo SDK 57 and Expo Router, building cross-platform navigation for Android, iOS, and web requires nothing more than creating files in the right directory.

Quick Setup

New Expo projects ship with Expo Router pre-configured. Run npx create-expo-app@latest --template default to start with file-based routing out of the box. Existing projects can add it by installing expo-router and updating the entry point.

How File-Based Routing Works in Expo Router

Every file inside the app directory automatically becomes a route. The file path maps directly to the URL path, eliminating the need for a centralized navigation configuration. A file at app/settings.tsx creates a /settings route, while app/profile/edit.tsx maps to /profile/edit.

This approach offers three key advantages over traditional React Navigation setup:

  • Zero configuration: routes exist the moment a file is created
  • Automatic deep linking: every screen gets a URL, enabling sharing and testing
  • Type-safe navigation: TypeScript knows which routes exist at compile time
app/index.tsxtypescript
import { View, Text, StyleSheet } from 'react-native'
import { Link } from 'expo-router'

export default function HomeScreen() {
  return (
    <View style={styles.container}>
      <Text style={styles.title}>Welcome</Text>
      {/* Link maps directly to file path */}
      <Link href="/settings" style={styles.link}>
        Open Settings
      </Link>
      <Link href="/profile/edit" style={styles.link}>
        Edit Profile
      </Link>
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', padding: 24 },
  title: { fontSize: 28, fontWeight: 'bold', marginBottom: 16 },
  link: { fontSize: 16, color: '#61DAFB', marginTop: 12 },
})

The Link component handles navigation across all platforms. On the web, it renders an anchor tag with proper href attributes for SEO. On native platforms, it triggers stack-based navigation.

Project Structure and Layout Files

Expo Router uses _layout.tsx files to define navigation containers. Each directory can have its own layout, creating nested navigation hierarchies. The root layout wraps the entire app, while nested layouts control specific sections.

A typical project structure looks like this:

text
app/
  _layout.tsx          # Root layout (Stack or custom)
  index.tsx            # Home screen (/)
  (tabs)/              # Tab group (parentheses = route group)
    _layout.tsx        # Tab navigator
    home.tsx           # /home tab
    search.tsx         # /search tab
    profile.tsx        # /profile tab
  settings/
    _layout.tsx        # Settings stack layout
    index.tsx          # /settings
    notifications.tsx  # /settings/notifications
    privacy.tsx        # /settings/privacy

Route groups, directories wrapped in parentheses, organize files without affecting the URL. The (tabs) directory above creates a tab navigator, but the URLs remain /home, /search, and /profile rather than /tabs/home.

app/_layout.tsxtypescript
import { Stack } from 'expo-router'

export default function RootLayout() {
  return (
    <Stack
      screenOptions={{
        headerStyle: { backgroundColor: '#1a1a2e' },
        headerTintColor: '#ffffff',
        headerTitleStyle: { fontWeight: '600' },
      }}
    >
      <Stack.Screen name="index" options={{ title: 'Home' }} />
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      <Stack.Screen name="settings" options={{ title: 'Settings' }} />
    </Stack>
  )
}

The root layout also serves as the place to load fonts, initialize providers, and configure global settings.

Building Tab Navigation with Expo Router

Tab navigation requires a _layout.tsx file inside a route group. Expo Router provides both NativeTabs for platform-specific tab experiences and the standard Tabs component for most use cases. Since SDK 56, Native Tabs supports the disabled property to prevent tab selection while keeping tabs visible.

app/(tabs)/_layout.tsxtypescript
import { Tabs } from 'expo-router'
import { Ionicons } from '@expo/vector-icons'

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: '#61DAFB',
        tabBarInactiveTintColor: '#888',
        tabBarStyle: {
          backgroundColor: '#1a1a2e',
          borderTopColor: '#2d2d44',
        },
      }}
    >
      <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="profile"
        options={{
          title: 'Profile',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  )
}

Each tab screen file exports a standard React component. The tab bar icon, label, and badge are configured through the options prop in the layout.

Dynamic Routes and Route Parameters

Dynamic segments use square brackets in the filename. A file named [id].tsx matches any single segment, while [...slug].tsx catches all remaining segments.

app/product/[id].tsxtypescript
import { View, Text, StyleSheet } from 'react-native'
import { useLocalSearchParams, Stack } from 'expo-router'

export default function ProductScreen() {
  // Extract the dynamic parameter from the URL
  const { id } = useLocalSearchParams<{ id: string }>()

  return (
    <View style={styles.container}>
      <Stack.Screen options={{ title: `Product ${id}` }} />
      <Text style={styles.heading}>Product Details</Text>
      <Text style={styles.id}>ID: {id}</Text>
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24 },
  heading: { fontSize: 24, fontWeight: 'bold', marginBottom: 8 },
  id: { fontSize: 16, color: '#888' },
})

Navigating to /product/42 renders this screen with id set to "42". The useLocalSearchParams hook provides typed access to all route parameters.

For catch-all routes, [...slug].tsx captures entire path segments:

app/docs/[...slug].tsxtypescript
import { useLocalSearchParams } from 'expo-router'

export default function DocsScreen() {
  // /docs/getting-started/installation → slug = ['getting-started', 'installation']
  const { slug } = useLocalSearchParams<{ slug: string[] }>()

  return <DocViewer path={slug.join('/')} />
}

Ready to ace your React Native interviews?

Practice with our interactive simulators, flashcards, and technical tests.

Typed Routes for Compile-Time Safety

Expo Router generates route types automatically when typed routes is enabled. This catches broken links at compile time rather than at runtime.

Enable typed routes in app.json:

json
{
  "expo": {
    "experiments": {
      "typedRoutes": true
    }
  }
}

Once enabled, the href prop on Link and the argument to router.push() only accept valid route strings:

app/checkout.tsxtypescript
import { router } from 'expo-router'

function handleCheckout(cartId: string) {
  // TypeScript validates this route exists
  router.push(`/product/${cartId}`)

  // This would cause a compile error if /nonexistent doesn't exist
  // router.push('/nonexistent')
}

Typed routes pair well with useLocalSearchParams. The generated types ensure parameter names match between the route definition and the consuming component, preventing subtle bugs that surface only when navigating to a specific screen.

Modals in Expo Router are regular screens configured with presentation: 'modal' in the layout. This approach keeps the file-based convention intact: a modal is just another route.

app/_layout.tsxtypescript
import { Stack } from 'expo-router'

export default function RootLayout() {
  return (
    <Stack>
      <Stack.Screen name="index" />
      <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
      {/* Modal screen slides up from the bottom */}
      <Stack.Screen
        name="create-post"
        options={{
          presentation: 'modal',
          headerTitle: 'New Post',
        }}
      />
    </Stack>
  )
}
app/create-post.tsxtypescript
import { View, TextInput, Button, StyleSheet } from 'react-native'
import { router } from 'expo-router'
import { useState } from 'react'

export default function CreatePostModal() {
  const [title, setTitle] = useState('')

  const handleSubmit = () => {
    // Submit logic here
    router.back() // Dismiss the modal
  }

  return (
    <View style={styles.container}>
      <TextInput
        style={styles.input}
        placeholder="Post title"
        value={title}
        onChangeText={setTitle}
      />
      <Button title="Publish" onPress={handleSubmit} />
    </View>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24 },
  input: {
    borderWidth: 1,
    borderColor: '#333',
    borderRadius: 8,
    padding: 12,
    fontSize: 16,
    marginBottom: 16,
  },
})

Navigating to /create-post triggers the modal presentation. The router.back() call dismisses it, returning to the previous screen in the stack.

Programmatic Navigation and the Router API

Beyond the Link component, Expo Router provides an imperative API through the router object. This handles navigation triggered by business logic rather than user taps.

typescript
import { router } from 'expo-router'

// Push a new screen onto the stack
router.push('/profile/settings')

// Replace the current screen (no back button)
router.replace('/login')

// Go back to the previous screen
router.back()

// Navigate with parameters
router.push({
  pathname: '/product/[id]',
  params: { id: '42', source: 'recommendations' },
})

// Check if going back is possible
import { useRouter } from 'expo-router'

function BackButton() {
  const router = useRouter()
  return router.canGoBack() ? (
    <Button title="Back" onPress={() => router.back()} />
  ) : null
}

The router.push and router.replace distinction matters for authentication flows in React Native. After a successful login, router.replace('/dashboard') prevents the user from navigating back to the login screen.

Navigation vs. Redirect

router.replace() replaces the current entry in the navigation history. For authentication guards that redirect unauthenticated users, use <Redirect href="/login" /> inside the component render: it triggers during the render phase and works correctly with server-side rendering on web.

Middleware and Route Protection

Expo Router provides server middleware for route-level logic. The +middleware.ts file intercepts requests before they reach the route component, enabling authentication checks, redirects, and header manipulation.

app/+middleware.tstypescript
import { type MiddlewareRequest } from 'expo-router/server'

export function middleware(request: MiddlewareRequest) {
  const { pathname } = request.nextUrl

  // Protect dashboard routes
  const protectedPaths = ['/dashboard', '/settings', '/profile']
  const isProtected = protectedPaths.some(p => pathname.startsWith(p))

  if (isProtected) {
    const token = request.cookies.get('session')
    if (!token) {
      return Response.redirect(new URL('/login', request.url))
    }
  }

  return undefined // Continue to route
}

On native platforms where server middleware does not run, route protection relies on client-side guards. A common pattern wraps protected layouts with an authentication check:

app/(authenticated)/_layout.tsxtypescript
import { Redirect, Stack } from 'expo-router'
import { useAuth } from '@/hooks/useAuth'

export default function AuthenticatedLayout() {
  const { isLoggedIn, isLoading } = useAuth()

  if (isLoading) return null
  if (!isLoggedIn) return <Redirect href="/login" />

  return <Stack />
}
Platform Awareness

Server middleware runs only on web with server-side rendering. For native apps, always implement client-side route guards in layout components. Combining both approaches ensures consistent protection across all platforms.

SDK 56 Migration: React Navigation Fork

Starting with Expo SDK 56, Expo Router no longer depends on @react-navigation/* packages. If your project imports from React Navigation directly, update the imports:

Original PackageNew Location
@react-navigation/nativeexpo-router/react-navigation
@react-navigation/stackexpo-router/js-stack
@react-navigation/bottom-tabsexpo-router/js-tabs

Run the codemod to update imports automatically:

bash
npx expo-codemod sdk-56-expo-router-react-navigation-replace src

The runtime API remains unchanged. Only the module specifiers moved. Third-party libraries that still import from @react-navigation/core work automatically because Expo CLI rewrites those imports during bundling.

Sources

What Interviewers Expect About Expo Router

  • Expo Router maps each file in the app directory to a route automatically, eliminating manual navigation configuration
  • Layouts defined through _layout.tsx files create navigation hierarchies for stacks, tabs, and drawers
  • Dynamic routes with [param].tsx and catch-all routes with [...slug].tsx handle parameterized navigation with full TypeScript support
  • Typed routes catch broken navigation links at compile time when enabled in app.json
  • Modal screens, programmatic navigation via router.push/replace/back, and server middleware complete the routing toolkit
  • Route groups using parenthesized directories organize code without affecting URLs
  • Client-side route guards in layouts handle authentication on native, while server middleware covers web
  • Since SDK 56, imports from @react-navigation/* must be updated to expo-router/* equivalents

For more on React Native architecture and interview preparation, see the native modules module.

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Daily challenge

Can you spot the bug in React Native?

One real snippet, one hidden bug, one attempt a day. No account needed to try.

Anthony Fillion-Maillet

Written by

Anthony Fillion-Maillet

Founder of SharpSkill

Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.

Updated on October 6, 2026

Tags

#react-native
#expo
#expo-router
#navigation
#mobile-development
#tutorial

Share

Related articles