Expo Router fundamentally changes how React Native developers think about navigation. Instead of manually wiring up React Navigation stacks, tabs, and drawers with imperative configuration objects, you define routes by creating files in your /app directory — the same convention that made Next.js routing intuitive for web developers.
The result is a navigation system that supports deep linking out of the box, enables type-safe route parameters, and works identically on iOS, Android, and web.
File-Based Navigation Conventions
Expo Router automatically creates navigation routes based on your /app directory structure. The mapping is predictable:
app/index.tsx→ Home screen (/)app/settings.tsx→ Settings screen (/settings)app/details/[id].tsx→ Dynamic detail screen (/details/142)app/(tabs)/_layout.tsx→ Tab navigator wrapping child routesapp/+not-found.tsx→ 404 fallback screen
Directory Structure Example
app/
├── _layout.tsx # Root Stack navigator
├── index.tsx # Home screen (/)
├── (tabs)/
│ ├── _layout.tsx # Tab navigator
│ ├── index.tsx # Feed tab
│ ├── search.tsx # Search tab
│ └── profile.tsx # Profile tab
├── details/
│ └── [id].tsx # Dynamic route (/details/:id)
├── modal.tsx # Modal screen
└── +not-found.tsx # 404 handler
Configuring Deep Links
Expo Router includes built-in deep linking support. Universal links on iOS and App Links on Android work automatically when you configure your app.json:
{
"expo": {
"scheme": "venelxapp",
"web": {
"bundler": "metro"
},
"ios": {
"associatedDomains": ["applinks:app.venelx.com"]
},
"android": {
"intentFilters": [{
"action": "VIEW",
"data": [{ "scheme": "https", "host": "app.venelx.com" }],
"category": ["BROWSABLE", "DEFAULT"]
}]
}
}
}
This maps URLs like venelxapp://details/142 and https://app.venelx.com/details/142 directly to your dynamic detail screen. Expo Router parses the id parameter automatically.
Handling Nested Layout Stacks
Combine tab-bar navigation with detail screen stacks using parenthesized group directories. Groups create navigator nesting without adding URL segments:
// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: "#111111",
tabBarStyle: {
borderTopWidth: 4,
borderTopColor: "#111111",
backgroundColor: "#fdfbf7",
},
}}
>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarIcon: ({ color }) => <Ionicons name="home" size={24} color={color} />,
}}
/>
<Tabs.Screen
name="search"
options={{
title: "Search",
tabBarIcon: ({ color }) => <Ionicons name="search" size={24} color={color} />,
}}
/>
<Tabs.Screen
name="profile"
options={{
title: "Profile",
tabBarIcon: ({ color }) => <Ionicons name="person" size={24} color={color} />,
}}
/>
</Tabs>
);
}
Type-Safe Route Parameters
Expo Router provides typed useLocalSearchParams hooks that give you autocomplete and type checking for route parameters:
// app/details/[id].tsx
import { useLocalSearchParams } from "expo-router";
import { View, Text } from "react-native";
export default function DetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
return (
<View style={{ padding: 24 }}>
<Text style={{ fontSize: 24, fontWeight: "900" }}>
Detail #{id}
</Text>
</View>
);
}
Error Boundaries and Not-Found Handling
Production apps need graceful error handling. Expo Router supports ErrorBoundary exports and the +not-found.tsx convention:
// app/+not-found.tsx
import { Link, Stack } from "expo-router";
import { View, Text } from "react-native";
export default function NotFoundScreen() {
return (
<>
<Stack.Screen options={{ title: "Page Not Found" }} />
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text style={{ fontSize: 32, fontWeight: "900" }}>404</Text>
<Link href="/" style={{ marginTop: 16, color: "#4c8cfc" }}>
Go Home
</Link>
</View>
</>
);
}
For route-level error boundaries, export an ErrorBoundary component from any route file:
// app/details/[id].tsx
export function ErrorBoundary({ error }: { error: Error }) {
return (
<View style={{ flex: 1, justifyContent: "center", padding: 24 }}>
<Text style={{ fontSize: 18, fontWeight: "700", color: "red" }}>
Something went wrong: {error.message}
</Text>
</View>
);
}
For details on automating compilation and publishing in CI, read Expo Prebuild benefits and Shrinking React Native bundles.
References & Citations
- Expo Router Documentation: Expo Official Docs
- React Navigation Architecture: React Navigation Core
- Universal Links on iOS: Apple Developer Documentation
- App Store Dispatches: App Store Connect API