Expo Router in React Native: File-Based Navigation Complete Guide
Master Expo Router for React Native with this complete tutorial covering file-based routing, layouts, dynamic routes, typed navigation, and advanced patterns like modals and tabs in 2026.

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 55 and Expo Router v6, 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@sdk-55 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 — replacing the traditional App.tsx entry point.
Building Tab Navigation with Expo Router
Tab navigation requires a _layout.tsx file inside a route group. Expo Router v6 introduces NativeTabs for platform-specific tab experiences, but the standard Tabs component from Expo Router covers most use cases.
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. 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 v6 introduces 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.
Conclusion
- Expo Router v6 replaces manual navigation configuration with file-based conventions, mapping each file in the
appdirectory to a route automatically - Layouts defined through
_layout.tsxfiles create navigation hierarchies — stacks, tabs, and drawers — without centralized config - 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 round out the routing toolkit - Route groups using parenthesized directories organize code without affecting URLs, keeping the file tree clean as the app grows
- Client-side route guards in layouts handle authentication on native, while server middleware covers web — use both for cross-platform apps
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Tags
Share
Related articles

React Native vs Flutter: Complete 2026 Comparison
In-depth React Native vs Flutter comparison for 2026: performance, architecture, DX, costs. Guide to choosing the right cross-platform framework.

Top 30 React Native Interview Questions: Complete Guide 2026
The 30 most asked React Native interview questions. Detailed answers with code examples to land your mobile developer job.

React Native: Building a Complete Mobile App in 2026
Complete guide to developing iOS and Android mobile applications with React Native. From setup to deployment, all the fundamentals to get started.