Expo Router Basics
10 examples to get you started with Expo Router - 7 basic and 3 intermediate. File-based routes, _layout.tsx, and the mental model vs React Navigation.
Search across all documentation pages
10 examples to get you started with Expo Router - 7 basic and 3 intermediate. File-based routes, _layout.tsx, and the mental model vs React Navigation.
Scaffold an SDK 57 app with the default template - it ships Expo Router, TypeScript, and the recommended entry point.
npx create-expo-app@latest MyRouterApp --template default@sdk-57
cd MyRouterApp
npm install
npx expo install expo-routerConfirm the router entry and SDK pin:
{
"main": "expo-router/entry",
"dependencies": {
"expo": "~57.0.4",
"expo-router": "~6.0.8",
"react": "19.2.3",
"react-native": "0.86.0"
}
}Tooling: These examples target Expo SDK 57 (
expo~57.0.4), React Native 0.86.0, and React 19.2.3.
Every file under app/ becomes a route segment. The file name is the path - no central linking config required for standard screens.
app/
├── index.tsx → /
├── settings.tsx → /settings
├── profile/
│ └── index.tsx → /profile
└── orders/
├── index.tsx → /orders
└── [id].tsx → /orders/:id// app/settings.tsx
import { Text, View } from "react-native";
export default function SettingsScreen() {
return (
<View style={{ flex: 1, padding: 16 }}>
<Text>Settings</Text>
</View>
);
}app/index.tsx is the / route - the app's default landing screenapp/profile/index.tsx → /profile)[id].tsx is a dynamic segment - access id via useLocalSearchParams()features/ when logic growsRelated: Route Groups & Organizing
app/-(auth)and(tabs)without changing URLs | ../expo-rules/navigation-and-routing-rules/navigation-and-routing-rules.md - team routing conventions
_layout.tsx Owns the Navigator_layout.tsx files define navigators (Stack, Tabs, Drawer) for their directory. The root layout wraps global providers and exports the top-level navigator.
// app/_layout.tsx
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
export default function RootLayout() {
return (
<>
<StatusBar style="auto" />
<Stack>
<Stack.Screen name="index" options={{ title: "Home" }} />
<Stack.Screen name="settings" options={{ title: "Settings" }} />
</Stack>
</>
);
}Stack from expo-router is a file-based wrapper around @react-navigation/native-stack<Stack.Screen name="settings"> configures the screen that matches app/settings.tsxapp/orders/[id].tsx pushes onto the root stack unless a nested _layout.tsx says otherwiseRelated: Stack Navigation - headers,
screenOptions, and push/pop | ../architecture-design/feature-sliced-design-for-rn/feature-sliced-design-for-rn.md - keepapp/as the FSD app layer
React Navigation builds a navigator tree in code. Expo Router builds the same tree from the filesystem - you trade imperative createNativeStackNavigator for declarative file names.
// React Navigation (imperative tree)
const Stack = createNativeStackNavigator();
function AppNavigator() {
return (
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Settings" component={SettingsScreen} />
</Stack.Navigator>
);
}// Expo Router (filesystem tree)
app/
├── _layout.tsx → Stack navigator for this folder
├── index.tsx → "Home" screen (name derived from file)
└── settings.tsx → "Settings" screen| Concept | React Navigation | Expo Router |
|---|---|---|
| Screen registration | <Stack.Screen name="Settings" /> | app/settings.tsx exists |
| Nested navigator | Navigator inside a screen | app/(tabs)/_layout.tsx |
| Deep link path | Manual linking config | File path ≈ URL path |
| Screen options | options prop on Screen | export const options or <Stack.Screen options> |
useNavigation, useFocusEffect, and header APIs still applysettings.tsx registers as "settings" in the stackRelated: Link & href - declarative navigation without memorizing route names | Typed Routes - compile-time path safety
expo-router/entry Bootstrappackage.json main points Metro at Expo Router's entry instead of expo/AppEntry.js or a hand-written App.tsx.
{
"main": "expo-router/entry",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios"
}
}// You do NOT need App.tsx - the entry loads app/_layout.tsx automatically
// app/_layout.tsx is the true application root
import { Slot } from "expo-router";
export default function RootLayout() {
return <Slot />;
}expo-router/entry registers the root component, linking handlers, and error boundariesSlot renders the matched child route - use it when you want a layout without forcing a Stack/Tabs wrappermain: "expo-router/entry" breaks routing - the bundler will not discover app/ routesnpx expo install expo-router ensures the router version matches SDK 57's React Navigation peersRelated: ../project-setup/project-setup-basics/project-setup-basics.md - SDK pin and default template layout
Bracket syntax creates dynamic URL segments. Read params with useLocalSearchParams - validate before use.
// app/orders/[id].tsx
import { useLocalSearchParams } from "expo-router";
import { Text, View } from "react-native";
export default function OrderDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
return (
<View style={{ flex: 1, padding: 16 }}>
<Text>Order #{id}</Text>
</View>
);
}// Navigate programmatically (imperative)
import { router } from "expo-router";
router.push({ pathname: "/orders/[id]", params: { id: "42" } });[id] matches one segment - /orders/42 sets id: "42" (always strings from URLs)app/users/[userId]/posts/[postId].tsx exposes both paramsRelated: Typed Routes - generated
Hreftypes for param-safe navigation | ../expo-rules/navigation-and-routing-rules/navigation-and-routing-rules.md - param validation rules
index.tsx inside a folder is the default screen for that path segment - like index.html on the web.
app/
├── index.tsx → /
└── orders/
├── _layout.tsx → Stack for /orders/*
├── index.tsx → /orders (list)
└── [id].tsx → /orders/:id (detail)// app/orders/index.tsx
import { Link } from "expo-router";
import { Text, View } from "react-native";
const ORDERS = [{ id: "1" }, { id: "2" }];
export default function OrdersListScreen() {
return (
<View style={{ flex: 1, padding: 16, gap: 12 }}>
{ORDERS.map((order) => (
<Link key={order.id} href={{ pathname: "/orders/[id]", params: { id: order.id } }}>
Order #{order.id}
</Link>
))}
</View>
);
}/orders resolves to orders/index.tsx, not orders.tsx - pick one pattern per feature_layout.tsx wraps all siblings in that folder (index.tsx and [id].tsx)Related: Redirects & Index Routes - default landing and auth-gated entry | Link & href -
Linkvsrouter.push
Export options from a route file or configure screens in _layout.tsx - both feed React Navigation's screen options API.
// app/settings.tsx
import { Stack } from "expo-router";
import { Text, View } from "react-native";
export const options = {
title: "Settings",
headerLargeTitle: true,
};
export default function SettingsScreen() {
return (
<View style={{ flex: 1, padding: 16 }}>
<Text>Settings</Text>
</View>
);
}// app/_layout.tsx (alternative - centralize chrome)
import { Stack } from "expo-router";
export default function RootLayout() {
return (
<Stack
screenOptions={{
headerShown: true,
animation: "slide_from_right",
}}
/>
);
}screenOptions on the navigator sets defaults; per-file options overrides for one screenStack.Screen name="settings" options={{...}} in _layout.tsx is equivalent to exporting options from settings.tsxuseNavigation().setOptions() inside the screen when data loadsRelated: Stack Navigation - cookbook for headers, gestures, and presentation modes
The default SDK 57 template nests stacks under tabs - each tab folder can push detail screens without losing the tab bar.
app/
├── _layout.tsx
└── (tabs)/
├── _layout.tsx → Tabs navigator
├── index.tsx → Home tab
└── orders/
├── _layout.tsx → Stack inside the Orders tab
├── index.tsx → /orders list (tab root)
└── [id].tsx → /orders/:id (pushed, tab bar stays)// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
export default function TabLayout() {
return (
<Tabs screenOptions={{ headerShown: false }}>
<Tabs.Screen
name="index"
options={{ title: "Home", tabBarIcon: ({ color }) => <Ionicons name="home" size={24} color={color} /> }}
/>
<Tabs.Screen
name="orders"
options={{ title: "Orders", tabBarIcon: ({ color }) => <Ionicons name="list" size={24} color={color} /> }}
/>
</Tabs>
);
}// app/(tabs)/orders/_layout.tsx
import { Stack } from "expo-router";
export default function OrdersStackLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: "Orders" }} />
<Stack.Screen name="[id]" options={{ title: "Order" }} />
</Stack>
);
}(tabs)/_layout.tsx - each immediate child folder or file becomes a taborders/ - pushes [id] on top of orders/index while the tab bar remains visible(tabs) are a route group - they organize files without adding /tabs to the URLRelated: Tabs & Drawers - tab bar styling, drawer composition | Route Groups & Organizing
app/-(tabs)conventions
Route groups (auth) and (app) separate login flows from the main shell without changing URL paths.
app/
├── _layout.tsx
├── index.tsx → Redirect hub (see redirects article)
├── (auth)/
│ ├── _layout.tsx → Stack for login/signup
│ ├── login.tsx → /login
│ └── register.tsx → /register
└── (app)/
├── _layout.tsx → Authenticated shell (tabs or stack)
└── (tabs)/
├── _layout.tsx
└── index.tsx → / (when signed in)// 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>
);
}(auth) and (app) do not appear in URLs - /login not /auth/login_layout.tsx or index.tsx with <Redirect /> - see the redirects cookbook_layout.tsxRelated: Redirects & Index Routes - auth-gated entry | ../auth-session/mobile-auth-basics/mobile-auth-basics.md - session providers
Production apps keep app/ as a routing layer - screens, hooks, and API calls live in features/.
features/
└── orders/
├── index.ts
├── ui/
│ ├── OrdersListScreen.tsx
│ └── OrderDetailScreen.tsx
└── model/
└── useOrder.ts
app/
└── (tabs)/
└── orders/
├── index.tsx → re-export only
└── [id].tsx → re-export only// features/orders/index.ts
export { OrdersListScreen } from "./ui/OrdersListScreen";
export { OrderDetailScreen } from "./ui/OrderDetailScreen";// app/(tabs)/orders/index.tsx
export { OrdersListScreen as default } from "@/features/orders";// app/(tabs)/orders/[id].tsx
export { OrderDetailScreen as default } from "@/features/orders";index.ts - no cross-feature deep importsapp/ is the app layer, not a god folderRelated: Expo Router Fundamentals Best Practices - full section summary | ../project-setup/folder-structure-for-features/folder-structure-for-features.md - scaling past the starter tree
Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026