Expo Router
File-based routing: a file in app/ is a route, _layout.tsx is the navigator, and Link is not an anchor tag.
<Link href="/settings">Settings</Link> // same as useRouter().navigate('/settings')Expo Router gives the app a file system for routes. A file in the app directory is a route: app/settings.tsx becomes /settings. There is no route table to register and nothing to keep in sync.
The conventions
index.tsx— the default route of its folder._layout.tsx— not a screen. It renders the navigator that wraps every route in its folder.app/_layout.tsxis the root layout and the app's entry point.[id].tsx— a dynamic segment, matching one path segment.[...rest].tsx— a catch-all, matching the rest of the path.(group)— a folder whose name is in parentheses. It groups files and contributes nothing to the path.+not-found.tsx— the route for anything that matches no file.
Moving between routes
Link is the declarative form and useRouter is the same thing in code. Link is not an anchor tag: on iOS and Android it renders a Text and accepts Text props, and href names a route inside app rather than a URL a server answers. Pass asChild to hand the press to your own Pressable instead.
Underneath
Expo Router is a layer over React Navigation. The layouts you write produce React Navigation navigators, so the screen options are the ones in the React Navigation documentation. Route typing is still opt-in in SDK 57: set experiments.typedRoutes to true in the app config and the CLI generates an expo-env.d.ts that types Link and the router hooks.
Example
// app/index.tsx - the route at "/"
// On screen: a "Home" heading with two tappable lines under it; the first opens
// the settings screen, the second opens the detail screen for order 1042.
import { Link } from 'expo-router';
import { StyleSheet, Text, View } from 'react-native';
export default function Index() {
return (
<View style={styles.container}>
<Text style={styles.title}>Home</Text>
{/* href is a route path inside app/, not a URL a server answers.
The string child is fine here: Link renders a Text on native. */}
<Link href="/settings" style={styles.link}>
Settings
</Link>
{/* A dynamic route, written as the file's path plus its params.
The plain string "/order/1042" works too and means the same thing. */}
<Link href={{ pathname: '/order/[id]', params: { id: '1042' } }} style={styles.link}>
Order 1042
</Link>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 24, gap: 16 },
title: { fontSize: 24, fontWeight: '600' },
link: { fontSize: 16, color: '#0a7ea4' },
});When to use it
- Adding a new screen to an app without touching any navigation configuration.
- Turning a list row into a tappable link to that record's detail screen.
- Deciding where app-wide providers such as a theme or a font loader should live.
More examples
The root layout
This is where app-wide setup belongs: a theme provider, a font load, an error boundary. Every route renders inside it, so anything you wrap here applies everywhere, which is the only kind of inheritance this framework has.
// app/_layout.tsx - the one route file that wraps every other route
import { Stack } from 'expo-router';
export default function RootLayout() {
// <Stack /> is the self-closing shorthand for <Stack></Stack>. Identical.
// Files inside app/ become routes whether or not you list them here;
// Stack.Screen exists to give a route options.
return (
<Stack screenOptions={{ headerTitleAlign: 'center' }}>
<Stack.Screen name="index" options={{ title: 'Home' }} />
<Stack.Screen name="settings" options={{ title: 'Settings' }} />
{/* +not-found.tsx catches every path that matches no file */}
<Stack.Screen name="+not-found" options={{ title: 'Not found' }} />
</Stack>
);
}Link, asChild, and the imperative form
Reach for asChild the moment a link needs to look like a button, a card or a list row. Reach for useRouter when something has to happen before the navigation, such as saving a draft.
// app/settings.tsx
import { Link, useRouter } from 'expo-router';
import { Pressable, StyleSheet, Text, View } from 'react-native';
export default function Settings() {
const router = useRouter(); // the imperative form of Link
return (
<View style={styles.container}>
{/* asChild stops Link rendering its own Text and hands the press
to the child instead, so the child keeps full control of layout. */}
<Link href="/" asChild>
<Pressable style={styles.button}>
<Text style={styles.buttonText}>Home</Text>
</Pressable>
</Link>
{/* The same destination, decided in code rather than in markup.
Use this form when a condition or an await comes first. */}
<Pressable style={styles.button} onPress={() => router.push('/')}>
<Text style={styles.buttonText}>Home, imperatively</Text>
</Pressable>
<Pressable style={styles.button} onPress={() => router.back()}>
<Text style={styles.buttonText}>Back</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 24, gap: 12 },
button: { padding: 14, borderRadius: 8, backgroundColor: '#0a7ea4' },
buttonText: { color: '#fff', fontSize: 16, textAlign: 'center' },
});A route inside a group
Shows the two things that confuse people first: a group folder that is invisible in the path, and a Link used as a list row. Each row is tappable and opens the matching post route.
// app/(tabs)/index.tsx
// The folder name is in parentheses, so this file's path is "/", not "/(tabs)".
// Groups organise files and give them a shared layout; they never appear
// in a path, which means moving a file into one does not break its links.
import { Link } from 'expo-router';
import { FlatList, StyleSheet, Text } from 'react-native';
type Post = { id: string; title: string };
const POSTS: Post[] = [
{ id: 'a1', title: 'Shipping on Friday' },
{ id: 'a2', title: 'Why the build broke' },
];
export default function Feed() {
return (
<FlatList
data={POSTS}
keyExtractor={(post) => post.id}
// A list of unknown length belongs in a FlatList, never a ScrollView.
renderItem={({ item }) => (
<Link href={`/post/${item.id}`} style={styles.row}>
{item.title}
</Link>
)}
/>
);
}
const styles = StyleSheet.create({
row: { padding: 16, fontSize: 16 },
});
Discussion