Fix: Expo Router Not Working — Routes Not Matching, Layout Nesting Wrong, or Deep Links Failing
Quick Answer
How to fix Expo Router issues — file-based routing, layout routes, dynamic segments, tabs and stack navigation, modal routes, authentication flows, and deep linking configuration.
The Problem
A route file exists but navigating to it shows “Unmatched Route”:
app/
├── _layout.tsx
├── index.tsx # /
└── settings.tsx # /settings → "Unmatched Route"Or tab navigation doesn’t render the correct screens:
// Tabs render but tapping a tab shows blank contentOr router.push() navigates but the back button doesn’t work correctly:
router.push('/details/123');
// Screen shows but pressing back goes to the wrong screenWhy This Happens
Expo Router uses file-system routing for React Native, similar to Next.js App Router:
- File names map directly to routes —
app/settings.tsxcreates the/settingsroute. A misplaced file (e.g.,src/settings.tsxoutsideapp/) won’t register. _layout.tsxdefines the navigation structure — layouts wrap child routes. A_layout.tsxwith<Stack>creates stack navigation. With<Tabs>, it creates tab navigation. Missing or misconfigured layouts cause blank screens.- Group routes
(name)don’t affect the URL — directories wrapped in parentheses like(tabs)or(auth)are organizational — they don’t add URL segments. But they must have their own_layout.tsx. - Dynamic segments use
[param]syntax —app/user/[id].tsxmatches/user/123. The brackets are part of the filename, not optional.
Fix 1: Basic File Structure
app/
├── _layout.tsx # Root layout (Stack navigator)
├── index.tsx # / (home screen)
├── about.tsx # /about
├── settings.tsx # /settings
├── (tabs)/
│ ├── _layout.tsx # Tab layout
│ ├── index.tsx # / (home tab)
│ ├── explore.tsx # /explore (explore tab)
│ └── profile.tsx # /profile (profile tab)
├── user/
│ ├── _layout.tsx # Stack for user routes
│ ├── [id].tsx # /user/123 (dynamic)
│ └── edit.tsx # /user/edit
├── (auth)/
│ ├── _layout.tsx # Auth layout (no tabs)
│ ├── login.tsx # /login
│ └── register.tsx # /register
└── modal.tsx # Modal route// app/_layout.tsx — root layout
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{ presentation: 'modal', title: 'Modal' }}
/>
</Stack>
);
}Fix 2: Tab Navigation
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#3b82f6',
tabBarInactiveTintColor: '#9ca3af',
tabBarStyle: {
backgroundColor: '#ffffff',
borderTopColor: '#e5e7eb',
},
headerStyle: { backgroundColor: '#ffffff' },
headerShadowVisible: false,
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home-outline" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="explore"
options={{
title: 'Explore',
tabBarIcon: ({ color, size }) => (
<Ionicons name="compass-outline" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
tabBarIcon: ({ color, size }) => (
<Ionicons name="person-outline" size={size} color={color} />
),
}}
/>
</Tabs>
);
}
// app/(tabs)/index.tsx — home tab
import { View, Text } from 'react-native';
export default function HomeScreen() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Home Screen</Text>
</View>
);
}Fix 3: Navigation and Dynamic Routes
// app/user/[id].tsx — dynamic route
import { useLocalSearchParams, Stack } from 'expo-router';
import { View, Text } from 'react-native';
export default function UserProfile() {
const { id } = useLocalSearchParams<{ id: string }>();
return (
<View style={{ flex: 1, padding: 16 }}>
<Stack.Screen options={{ title: `User ${id}` }} />
<Text>User ID: {id}</Text>
</View>
);
}// Navigate programmatically
import { router, Link } from 'expo-router';
function NavigationExamples() {
return (
<View>
{/* Link component */}
<Link href="/about">About</Link>
<Link href="/user/123">User 123</Link>
<Link href={{ pathname: '/user/[id]', params: { id: '456' } }}>User 456</Link>
{/* Push (adds to stack) */}
<Button title="Go to Settings" onPress={() => router.push('/settings')} />
{/* Replace (replaces current screen) */}
<Button title="Replace" onPress={() => router.replace('/home')} />
{/* Back */}
<Button title="Go Back" onPress={() => router.back()} />
{/* Navigate (smart — push or back depending on history) */}
<Button title="Navigate" onPress={() => router.navigate('/explore')} />
{/* With params */}
<Button
title="Open User"
onPress={() => router.push({ pathname: '/user/[id]', params: { id: '789' } })}
/>
{/* Open modal */}
<Button title="Open Modal" onPress={() => router.push('/modal')} />
{/* Dismiss modal */}
<Button title="Close" onPress={() => router.dismiss()} />
</View>
);
}Fix 4: Authentication Flow
// app/(auth)/_layout.tsx
import { Stack } from 'expo-router';
export default function AuthLayout() {
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="login" />
<Stack.Screen name="register" />
</Stack>
);
}
// app/_layout.tsx — redirect based on auth state
import { Stack, Redirect } from 'expo-router';
import { useAuth } from '@/hooks/useAuth';
export default function RootLayout() {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) return <LoadingScreen />;
return (
<Stack>
{isAuthenticated ? (
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
) : (
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
)}
<Stack.Screen name="modal" options={{ presentation: 'modal' }} />
</Stack>
);
}
// Or use Redirect component in individual screens
// app/(tabs)/index.tsx
import { Redirect } from 'expo-router';
import { useAuth } from '@/hooks/useAuth';
export default function HomeScreen() {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) return <Redirect href="/login" />;
return <View>...</View>;
}Fix 5: Deep Linking
// app.json — configure deep linking
{
"expo": {
"scheme": "myapp",
"web": {
"bundler": "metro"
},
"plugins": [
[
"expo-router",
{
"origin": "https://myapp.com"
}
]
]
}
}// Deep links map to file routes:
// myapp://settings → app/settings.tsx
// myapp://user/123 → app/user/[id].tsx
// https://myapp.com/about → app/about.tsx
// Handle deep links in code
import { useURL } from 'expo-linking';
import { router } from 'expo-router';
import { useEffect } from 'react';
function DeepLinkHandler() {
const url = useURL();
useEffect(() => {
if (url) {
// Expo Router handles most deep links automatically
// Custom handling for specific cases:
const path = new URL(url).pathname;
if (path.startsWith('/invite/')) {
const code = path.replace('/invite/', '');
handleInvite(code);
}
}
}, [url]);
return null;
}Fix 6: API Routes (Server Functions)
// app/api/users+api.ts — API route (Expo web only)
export async function GET(request: Request) {
const users = await db.query.users.findMany();
return Response.json(users);
}
export async function POST(request: Request) {
const body = await request.json();
const user = await db.insert(users).values(body).returning();
return Response.json(user[0], { status: 201 });
}
// app/api/users/[id]+api.ts — dynamic API route
export async function GET(request: Request, { id }: { id: string }) {
const user = await db.query.users.findFirst({ where: eq(users.id, id) });
if (!user) return Response.json({ error: 'Not found' }, { status: 404 });
return Response.json(user);
}Still Not Working?
“Unmatched Route” — the file doesn’t exist in the app/ directory or the filename is wrong. Check: app/settings.tsx creates /settings, app/user/[id].tsx creates /user/:id. Files outside app/ are not routes.
Tab shows blank screen — the Tabs.Screen name prop must match the file name exactly. <Tabs.Screen name="index" /> maps to app/(tabs)/index.tsx. A mismatch between the name and the filename shows nothing.
Back navigation goes to wrong screen — router.push() always adds to the stack. Use router.replace() to swap the current screen, or router.navigate() which intelligently goes back if the route is already in the stack.
Layouts not nesting correctly — each directory that contains routes needs its own _layout.tsx. Group directories (name) also need layouts. If a layout is missing, child routes may not render or may skip the intended navigation structure.
For related mobile issues, see Fix: Expo Not Working and Fix: NativeWind Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: React Native Paper Not Working — Theme Not Applying, Icons Missing, or Components Unstyled
How to fix React Native Paper issues — PaperProvider setup, Material Design 3 theming, custom color schemes, icon configuration, dark mode, and Expo integration.
Fix: React Navigation Not Working — Screens Not Rendering, TypeScript Errors, or Gestures Broken
How to fix React Navigation issues — stack and tab navigator setup, TypeScript typing, deep linking, screen options, nested navigators, authentication flow, and performance optimization.
Fix: NativeWind Not Working — Styles Not Applying, Dark Mode Broken, or Metro Bundler Errors
How to fix NativeWind issues — Tailwind CSS for React Native setup, Metro bundler configuration, className prop, dark mode, responsive styles, and Expo integration.
Fix: Tamagui Not Working — Styles Not Applying, Compiler Errors, or Web/Native Mismatch
How to fix Tamagui UI kit issues — setup with Expo, theme tokens, styled components, animations, responsive props, media queries, and cross-platform rendering.