Expo Router
File-based routing deep dive with route groups, layouts, stack vs tabs, typed routes, and linking.
Expo Router
ScaleRocket Mobile uses Expo Router for file-based routing. Every file in the app/ directory automatically becomes a navigable screen.
How It Works
Expo Router maps files to routes:
app/
├── index.tsx → /
├── about.tsx → /about
├── settings/
│ ├── index.tsx → /settings
│ └── profile.tsx → /settings/profileRoute Groups
Parentheses () create groups that share a layout without affecting the URL:
app/
├── (auth)/
│ ├── _layout.tsx # Stack navigator for auth screens
│ ├── login.tsx → /login
│ └── register.tsx → /register
├── (tabs)/
│ ├── _layout.tsx # Tab navigator for main app
│ ├── index.tsx → /
│ └── profile.tsx → /profileThe group name (auth) does not appear in the URL. /login is the route, not /(auth)/login.
Layouts
Every _layout.tsx file defines navigation structure for its directory:
Stack Layout (Auth)
// 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.Screen name="forgot-password" />
</Stack>
);
}Tab Layout (Main App)
// 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, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: "Profile",
tabBarIcon: ({ color, size }) => (
<Ionicons name="person" size={size} color={color} />
),
}}
/>
</Tabs>
);
}Navigation
Use the router object or Link component:
import { router } from "expo-router";
import { Link } from "expo-router";
// Programmatic navigation
router.push("/profile");
router.replace("/login"); // Replace current screen (no back)
router.back(); // Go back
// Declarative navigation
<Link href="/profile">
<Text>Go to Profile</Text>
</Link>Dynamic Routes
Square brackets [] create dynamic segments:
app/
├── user/
│ └── [id].tsx → /user/123// app/user/[id].tsx
import { useLocalSearchParams } from "expo-router";
export default function UserScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
return <Text>User ID: {id}</Text>;
}Typed Routes
Enable typed routes in app.json for autocomplete:
{
"expo": {
"experiments": {
"typedRoutes": true
}
}
}After enabling, TypeScript will autocomplete route paths:
router.push("/profile"); // ✓ Valid route
router.push("/nonexist"); // ✗ Type errorModal Screens
Present screens as modals:
// app/_layout.tsx
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{ presentation: "modal", headerTitle: "Modal" }}
/>
</Stack>// Navigate to modal from anywhere
router.push("/modal");Deep Linking
Expo Router handles deep links automatically. Configure your scheme in app.json:
{
"expo": {
"scheme": "scalerocket"
}
}The URL scalerocket:///profile opens the profile screen. See the Deep Linking guide for full setup.
Not Found Screen
Add a catch-all for unmatched routes:
// app/+not-found.tsx
export default function NotFound() {
return (
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
<Text>Page not found</Text>
<Link href="/">Go home</Link>
</View>
);
}