This is a series of notes about Expo Router: how to set it up, how expo-router/entry works, the file-based routing model, APIs, and advanced usage patterns. this is just some notes for myself to study this technology to Learn more go to expo.dev.
References are linked inline to the official Expo docs.
If you’re starting fresh, the fastest way is to create an Expo app that already includes Expo Router:
npx create-expo-app@latest
cd your-app
npx expo start
If you’re adding Expo Router to an existing project, install dependencies and set the entry:
npx expo install expo-router react-native-safe-area-context react-native-screens expo-linking expo-constants expo-status-bar
Set your app entry to expo-router/entry (package.json):
{
"main": "expo-router/entry"
}
If you want a custom entry to initialize analytics, etc., create index.js and import expo-router/entry last, then point package.json main to index.js. The key is: import ‘expo-router/entry’ last so your side effects initialize first. See docs: Install Expo Router → “Setup entry point”
if you are creating using create-expo-router it comes pre-imported by default in the index.js alone so you just add anything that you want to add before it.
docs.expo.dev/router/installation.
// index.js
import './init-analytics';
import 'react-native-gesture-handler'; // example polyfill
// import ... ...
import 'expo-router/entry'; // and just put expo-router in the end
file-based-routes of expo-router/entry registers the Router as your app’s root and wires the router to a dynamic route manifest generated from your app directory at building or running time.The Router discovers routes via require.context on the ./app directory. This behavior depends on a Babel plugin (expo-router/babel) and Metro’s support for context modules.
Troubleshooting docs and background here: docs.expo.dev/router/reference/troubleshooting. The “Setup entry point” behavior is in the install guide: docs.expo.dev/router/installation. Expo Router relies on Metro and the expo/metro-config integration that enables context modules; see the troubleshooting note for require.context and Metro config.
Internally, Expo Router layers on top of React Navigation. File-based routes are compiled into a navigation tree with stacks/tabs/drawers as defined by your _layout files. for introduction and reference: docs.expo.dev/router/introduction, docs.expo.dev/versions/latest/sdk/router.
Create an app/ directory in your project root folder. Any file under app/ becomes a route; _layout files define navigators.
Notation reference: docs.expo.dev/router/basics/notation. File-based routing overview: docs.expo.dev/develop/file-based-routing.
Basic home route:
// app/index.tsx
import { View, Text, StyleSheet } from 'react-native';
export default function Home() {
return (
<View style={styles.container}>
<Text>Home Page Display</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center', justifyContent: 'center' }
});
Root layout with a Stack navigator:
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack
screenOptions={{
headerTitleAlign: 'center'
}}
/>
);
}
Core concepts explain why _layout is “rendered before any other route” : docs.expo.dev/router/basics/core-concepts.
Stacks, Tabs, and Drawers are expressed by nested _layout.tsx files inside the directories.
Tabs example:
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
export default function TabsLayout() {
return (
<Tabs screenOptions={{ headerShown: false }}>
<Tabs.Screen name="index" options={{ title: 'Main' }} />
<Tabs.Screen name="settings" options={{ title: 'Settings' }} />
</Tabs>
);
}
// app/(tabs)/index.tsx
import { View, Text } from 'react-native';
export default function Main() {
return <View><Text>Main page</Text></View>;
}
// app/(tabs)/settings.tsx
export default function Settings() {
return <View><Text>Settings</Text></View>;
}
Because (tabs) is a group, the URLs are / and /settings. More patterns are in Router 101 and Navigation patterns. API reference for Stack and Tabs usage: docs.expo.dev/versions/latest/sdk/router.
Create a dynamic segment with square brackets. Then link to it and read params.
// app/user/[id].tsx
import { useLocalSearchParams } from 'expo-router';
import { Text, View } from 'react-native';
export default function User() {
const { id } = useLocalSearchParams();
return <View><Text>User ID: {id}</Text></View>;
}
// app/index.tsx (link to dynamic route)
import { Link } from 'expo-router';
import { View } from 'react-native';
export default function Home() {
return (
<View>
<Link href="/user/1">View User 1</Link>
<Link href={{ pathname: '/user/[id]', params: { id: 'bacon' } }}>
View User bacon
</Link>
</View>
);
}
Dynamic routing guide: docs.expo.dev/develop/dynamic-routes.
[NOTE]:catch-alls use […slug].
// Declarative
import { Link } from 'expo-router';
<Link href="/settings" replace>Go to Settings</Link>
// Imperative
import { useRouter } from 'expo-router';
const router = useRouter();
router.push('/settings');
the Router API reference: docs.expo.dev/versions/latest/sdk/router
[Note]Useful hooks:
API reference for these hooks: docs.expo.dev/versions/latest/sdk/router. URL param handling: docs.expo.dev/router/reference/url-parameters
// app/search.tsx
import { useLocalSearchParams } from 'expo-router';
import { Text } from 'react-native';
export default function Search() {
const { q } = useLocalSearchParams<{ q?: string }>();
return <Text>Query: {q ?? 'none'}</Text>;
}
If you enable typed routes, you can strongly type route params and hrefs across the app. URL parameter docs: docs.expo.dev/router/reference/url-parameters.
Typed routes make your Link and router calls type-safe and catch invalid hrefs at compile time. Enable the experiment:
// app.json
{
"expo": {
"experiments": {
"typedRoutes": true
}
}
}
Run the dev server once; Expo CLI generates types (expo-env.d.ts) and augments your tsconfig includes. Then:
import { Link, useLocalSearchParams, useRouter } from 'expo-router';
// Type-checked hrefs
<Link href="/about" />
<Link href={{ pathname: '/user/[id]', params: { id: '123' }}} />
// Errors:
// <Link href="/usser/1" /> // misspelled route (TypeScript error)
// <Link href="/user/[id]" /> // dynamic route requires params object
You can also type search params per-route:
const { profile, search } = useLocalSearchParams<'app/(search)/[profile]/[...search].tsx'>();
Typed routes guide: docs.expo.dev/router/reference/typed-routes.
See notation doc for an overview: docs.expo.dev/router/basics/notation. Explore Redirect and protected routes patterns in the navigation sections of the docs.
Simple redirect example:
// app/protected/index.tsx
import { Redirect } from 'expo-router';
import { useAuth } from '../providers/auth';
export default function Protected() {
const { user } = useAuth();
if (!user) return <Redirect href="/login" />;
return /* protected UI */;
}
Route groups let you duplicate or share screens across different layouts (common in native apps where a profile is viewable from multiple tabs). You can use overlapping groups or the array syntax to “duplicate” children.
Example: a single user route in both the home and search groups via array syntax:
// app/(home,search)/_layout.tsx
export const unstable_settings = {
initialRouteName: 'home',
search: { initialRouteName: 'search' }
};
export default function DynamicLayout({ segment }: { segment: string }) {
if (segment === '(search)') {
return <SearchStack />;
}
return <Stack />;
}
// app/(home,search)/[user].tsx
export default function User() { /* ... */ }
This produces app/(home)/[user].tsx and app/(search)/[user].tsx in memory and lets the same URL be rendered under different layouts. See “Shared routes” and array syntax: docs.expo.dev/router/advanced/shared-routes.
To make modals, place them in the same Stack and set screen options to modal presentation:
// app/_layout.tsx
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: 'Home' }} />
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
</Stack>
);
}
// app/modal.tsx
export default function Modal() { /* ... */ }
If your initial route/back button behavior is odd, set unstable_settings.initialRouteName at the layout level:
// app/_layout.tsx
export const unstable_settings = { initialRouteName: 'index' };
This is a known gotcha; see troubleshooting “Missing back button”: docs.expo.dev/router/reference/troubleshooting.
For native deep links, set a scheme in app.json and Expo Router will map URLs to routes automatically:
// app.json
{
"expo": {
"scheme": "your-app-scheme"
}
}
Installation doc step for project configuration: docs.expo.dev/router/installation.
Key docs for internals and setup behavior:
Here’s a compact app skeleton demonstrating the major features.
// app/_layout.tsx
import { Stack } from 'expo-router';
export const unstable_settings = { initialRouteName: '(tabs)' };
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Compose' }} />
</Stack>
);
}
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
export default function TabsLayout() {
return (
<Tabs>
<Tabs.Screen name="index" options={{ title: 'Feed' }} />
<Tabs.Screen name="search" options={{ title: 'Search' }} />
<Tabs.Screen name="settings" options={{ title: 'Settings' }} />
</Tabs>
);
}
// app/(tabs)/index.tsx
import { Link } from 'expo-router';
import { View, Text, Button } from 'react-native';
import { useRouter } from 'expo-router';
export default function Feed() {
const router = useRouter();
return (
<View>
<Text>Feed</Text>
<Link href="/user/42">Go to User 42</Link>
<Button title="Open Compose Modal" onPress={() => router.push('/modal')} />
</View>
);
}
// app/user/[id].tsx
import { useLocalSearchParams, Link } from 'expo-router';
import { View, Text } from 'react-native';
export default function User() {
const { id } = useLocalSearchParams<{ id: string }>();
return (
<View>
<Text>User {id}</Text>
<Link href={{ pathname: '/user/[id]', params: { id: String(Number(id) + 1) } }}>
Next user
</Link>
</View>
);
}
// app/modal.tsx
import { View, Text, TextInput, Button } from 'react-native';
import { useRouter } from 'expo-router';
export default function Compose() {
const router = useRouter();
return (
<View>
<Text>Compose</Text>
<TextInput placeholder="Write something..." />
<Button title="Send" onPress={() => router.dismiss()} />
</View>
);
}
// app/(tabs)/search.tsx
import { useLocalSearchParams, Link } from 'expo-router';
import { View, Text } from 'react-native';
export default function Search() {
const { q } = useLocalSearchParams<{ q?: string }>();
return (
<View>
<Text>Search query: {q ?? 'none'}</Text>
<Link href="/(tabs)/search?q=expo">Search "expo"</Link>
</View>
);
}
// app/+not-found.tsx
import { Link } from 'expo-router';
import { View, Text } from 'react-native';
export default function NotFound() {
return (
<View>
<Text>Oops, that route does not exist.</Text>
<Link href="/">Go Home</Link>
</View>
);
}
Enable typed routes in app.json and enjoy typed hrefs.
Expo Router is a superset on top of React Navigation. If you need advanced drawer control, direct stack actions, or highly customized transitions, use useNavigation() or screenOptions from React Navigation as usual. The Router keeps those escape hatches available while giving you a fast, typed, URL-first developer experience. here is the Router API reference: docs.expo.dev/versions/latest/sdk/router.