Expo Router is the most-searched Expo topic for a reason: it makes navigation feel like the web. Instead of registering navigators and screens by hand, you create files in an app/ folder and each one becomes a route. This guide covers everything you need to ship real navigation — stacks, tabs, dynamic routes, params, and programmatic navigation — with minimal SDK 54+ examples.
How file-based routing works
The rule is simple: a file in app/ is a screen, and its path is its URL.
app/
_layout.tsx → the root navigator
index.tsx → "/"
about.tsx → "/about"
product/
[id].tsx → "/product/:id" (dynamic)
(tabs)/
_layout.tsx → tab navigator
home.tsx → "/home"
settings.tsx → "/settings"
Folders wrapped in parentheses like (tabs) are route groups — they organize files without adding a segment to the URL.
New to project layout? See our companion guide on the Expo Router folder structure.
Set up the root layout
Every app/ directory needs a root _layout.tsx that defines the top-level navigator. A stack is the most common choice:
// app/_layout.tsx
import { Stack } from "expo-router"
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: "Home" }} />
<Stack.Screen name="product/[id]" options={{ title: "Product" }} />
</Stack>
)
}
Link between screens
Use the <Link> component for declarative navigation — it works just like an anchor tag:
// app/index.tsx
import { Link } from "expo-router"
import { Text, View } from "react-native"
export default function Home() {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center", gap: 12 }}>
<Text>Home screen</Text>
<Link href="/about">About</Link>
<Link href="/product/42">View product #42</Link>
</View>
)
}
Dynamic routes and reading params
A file named [id].tsx matches any value in that segment. Read the value with useLocalSearchParams():
// app/product/[id].tsx
import { useLocalSearchParams } from "expo-router"
import { Text, View } from "react-native"
export default function Product() {
const { id } = useLocalSearchParams<{ id: string }>()
return (
<View>
<Text>Showing product {id}</Text>
</View>
)
}
Navigate programmatically
For navigation triggered by logic (after a form submit, a login, etc.) use the useRouter() hook:
import { useRouter } from "expo-router"
import { Button } from "react-native"
export default function LoginButton() {
const router = useRouter()
return (
<Button
title="Sign in"
onPress={() => {
// ...run your auth logic, then:
router.replace("/home") // replace() so users can't go "back" to login
}}
/>
)
}
router.push() adds a screen to the stack, router.replace() swaps the current one, and router.back() pops.
Add tab navigation
Create a (tabs) group with its own _layout.tsx using Tabs:
// app/(tabs)/_layout.tsx
import { Tabs } from "expo-router"
export default function TabLayout() {
return (
<Tabs>
<Tabs.Screen name="home" options={{ title: "Home" }} />
<Tabs.Screen name="settings" options={{ title: "Settings" }} />
</Tabs>
)
}
Any file inside (tabs)/ — like home.tsx and settings.tsx — becomes a tab.
Common gotchas
- Nothing renders? You must have an
app/_layout.tsx. It's the entry point. - Typed routes: enable
experiments.typedRoutesin app.json to get autocomplete and type-safety onhrefvalues. - Deep linking: set a
schemein app.json (e.g."scheme": "myapp") so URLs likemyapp://product/42open the right screen automatically.
Next steps
Once navigation is wired up, most apps immediately need auth-gated routes. See how to add authentication to an Expo app for protecting screens with a root layout, and browse our Expo templates for full apps that already use Expo Router.