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 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.
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
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:
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/privacyRoute 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.
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.
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.
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:
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:
{
"expo": {
"experiments": {
"typedRoutes": true
}
}
}Once enabled, the href prop on Link and the argument to router.push() only accept valid route strings:
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.
Modal Screens and Presentation Options
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.
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>
)
}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.
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.
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.
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:
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 />
}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 Package | New Location |
|---|---|
@react-navigation/native | expo-router/react-navigation |
@react-navigation/stack | expo-router/js-stack |
@react-navigation/bottom-tabs | expo-router/js-tabs |
Run the codemod to update imports automatically:
npx expo-codemod sdk-56-expo-router-react-navigation-replace srcThe 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
- Expo Router Documentation - official guide and API reference
- Expo SDK 57 Changelog - current stable release notes
- Expo Router v56 Blog Post - React Navigation fork explanation
- SDK 55 to 56 Migration Guide - import migration steps
What Interviewers Expect About Expo Router
- Expo Router maps each file in the
appdirectory to a route automatically, eliminating manual navigation configuration - Layouts defined through
_layout.tsxfiles create navigation hierarchies for stacks, tabs, and drawers - Dynamic routes with
[param].tsxand catch-all routes with[...slug].tsxhandle 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 toexpo-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.
Can you spot the bug in React Native?
One real snippet, one hidden bug, one attempt a day. No account needed to try.

Written by
Anthony Fillion-MailletFounder of SharpSkill
Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.
Updated on October 6, 2026
Tags
Share
Related articles

React Native App Development Guide 2026: Building Production Apps and Interview Questions
Complete guide to React Native production development in 2026 covering Expo SDK 56, EAS Build, Hermes V1, the New Architecture, and interview preparation for React Native developers.

React Native App Development in 2026: Complete Guide and Interview Questions
Master React Native app development in 2026 with the New Architecture, Hermes V1, and Fabric. Includes practical code examples and common interview questions.

React Native 0.87 and SwiftPM in 2026: Modern iOS Build and Interview Questions
React Native 0.87 introduces experimental Swift Package Manager support, eliminating CocoaPods from iOS builds. This guide covers SwiftPM setup, the Strict TypeScript API, Metro 0.87 optimizations, and interview questions for senior mobile developers.