| name | expo-router |
| user-invocable | false |
| description | Use when implementing file-based routing in Expo with Expo Router. Covers app directory structure, navigation, layouts, dynamic routes, and deep linking. |
| allowed-tools | ["Read","Write","Edit","Bash","Grep","Glob"] |
Expo Router
Use this skill when implementing file-based routing with Expo Router, the recommended navigation solution for Expo apps.
Key Concepts
File-Based Routing
Routes are defined by file structure:
app/
_layout.tsx # Root layout
index.tsx # / route
about.tsx # /about route
(tabs)/ # Group (not in URL)
_layout.tsx # Tabs layout
home.tsx # /home
profile.tsx # /profile
users/
[id].tsx # /users/:id dynamic route
index.tsx # /users route
Basic Routes
import { View, Text } from 'react-native';
import { Link } from 'expo-router';
export default function Home() {
return (
<View>
<Text>Home Screen</Text>
<Link href="/about">Go to About</Link>
</View>
);
}
export default function About() {
return (
<View>
<Text>About Screen</Text>
</View>
);
}
Layouts
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: 'Home' }} />
<Stack.Screen name="about" options={{ title: 'About' }} />
</Stack>
);
}
Tab Navigation
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout() {
return (
<Tabs>
<Tabs.Screen
name="home"
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} />
),
}}
/>
</>
);
}
Best Practices
Dynamic Routes
import { useLocalSearchParams } from 'expo-router';
import { View, Text } from 'react-native';
export default function UserDetails() {
const { id } = useLocalSearchParams<{ id: string }>();
return (
<View>
<Text>User ID: {id}</Text>
</View>
);
}
<Link href="/users/123">View User</Link>
import { router } from 'expo-router';
router.push('/users/123');
Programmatic Navigation
import { router } from 'expo-router';
function MyComponent() {
const handlePress = () => {
router.push('/details');
router.push({
pathname: '/users/[id]',
params: { id: '123' },
});
router.replace('/login');
router.back();
};
return <Button title="Navigate" onPress={handlePress} />;
}
Type-Safe Routes
export type RootStackParamList = {
'/': undefined;
'/about': undefined;
'/users/[id]': { id: string };
'/posts/[id]': { id: string; title?: string };
};
import { router } from 'expo-router';
import type { RootStackParamList } from './types/navigation';
router.push({
pathname: '/users/[id]' as const,
params: { id: '123' },
});
Route Groups
Group routes without affecting URLs:
app/
(auth)/ # Group (not in URL)
login.tsx # /login
register.tsx # /register
_layout.tsx # Auth layout
(app)/ # Group (not in URL)
home.tsx # /home
profile.tsx # /profile
_layout.tsx # App layout
import { Stack } from 'expo-router';
export default function AuthLayout() {
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="login" />
<Stack.Screen name="register" />
</Stack>
);
}
Common Patterns
Authentication Flow
import { Slot, useRouter, useSegments } from 'expo-router';
import { useEffect } from 'react';
import { useAuth } from './hooks/useAuth';
export default function RootLayout() {
const { user, loading } = useAuth();
const segments = useSegments();
const router = useRouter();
useEffect(() => {
if (loading) return;
const inAuthGroup = segments[0] === '(auth)';
if (!user && !inAuthGroup) {
router.replace('/(auth)/login');
} else if (user && inAuthGroup) {
router.replace('/(app)/home');
}
}, [user, loading, segments]);
return <Slot />;
}
Modal Routes
import { Stack } from 'expo-router';
export default function RootLayout() {
return (
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{
presentation: 'modal',
title: 'Modal',
}}
/>
</Stack>
);
}
import { View, Text, Button } from 'react-native';
import { router } from 'expo-router';
export default function Modal() {
return (
<View>
<Text>Modal Content</Text>
< = = => router.back()} />
);
}
Deep Linking
{
"expo": {
"scheme": "myapp",
"plugins": ["expo-router"]
}
}
Search Params
import { useLocalSearchParams } from 'expo-router';
function ProductScreen() {
const { id, category, sort } = useLocalSearchParams<{
id: string;
category?: string;
sort?: string;
}>();
return (
<View>
<Text>Product: {id}</Text>
<Text>Category: {category}</Text>
<Text>Sort: {sort}</Text>
</View>
);
}
<Link href="/products/123?category=electronics&sort=price">
View Product
</Link>
Anti-Patterns
Don't Use React Navigation Directly
import { NavigationContainer } from '@react-navigation/native';
import { Stack } from 'expo-router';
Don't Nest Navigators Incorrectly
<Stack>
<Stack.Screen name="details" />
</Stack>
<Stack>
<Stack.Screen name="index" />
<Stack.Screen name="details" />
</Stack>
Don't Hardcode Routes
router.push('/users/123');
router.push('/prodcts/456');
const ROUTES = {
USER_DETAILS: (id: string) => `/users/${id}` as const,
PRODUCT_DETAILS: (id: string) => `/products/${id}` as const,
} as const;
router.push(ROUTES.USER_DETAILS('123'));
Related Skills
- expo-config: Configuring deep linking
- expo-modules: Using navigation with Expo modules