Expo Router file-based navigation patterns. Use when implementing navigation.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill covers Expo Router navigation for React Native.
Use this skill when:
FILE-BASED ROUTING - Routes are defined by file structure (like Next.js).
app/
├── (auth)/ # Route group (not in URL)
│ ├── login.tsx # /login
│ ├── register.tsx # /register
│ └── _layout.tsx # Layout for auth routes
├── (tabs)/ # Tab navigation group
│ ├── _layout.tsx # Tabs layout
│ ├── index.tsx # /
│ └── profile.tsx # /profile
├── settings/
│ ├── index.tsx # /settings
│ └── [id].tsx # /settings/123 (dynamic)
├── _layout.tsx # Root layout
├── +not-found.tsx # 404 page
└── [...missing].tsx # Catch-all route
import { Link } from 'expo-router';
<Link href="/profile">Go to Profile</Link>
// With params
<Link
href={{
pathname: '/user/[id]',
params: { id: '123' },
}}
>
View User
</Link>
// As child (for custom styling)
<Link href="/settings" asChild>
<TouchableOpacity>
<Text>Settings</Text>
</TouchableOpacity>
</Link>
import { useRouter } from 'expo-router';
function Component(): React.ReactElement {
const router = useRouter();
const handleNavigate = () => {
// Push new screen
router.push('/profile');
// Replace current screen
router.replace('/login');
// Go back
router.back();
// Navigate with params
router.push({
pathname: '/user/[id]',
params: { id: '123' },
});
};
return <Button onPress={handleNavigate}>Navigate</Button>;
}
// app/_layout.tsx
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
export default function RootLayout(): React.ReactElement {
return (
<>
<StatusBar style="auto" />
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{
presentation: 'modal',
headerTitle: 'Modal',
}}
/>
</Stack>
</>
);
}
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
export default function TabLayout(): React.ReactElement {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#3B82F6',
tabBarInactiveTintColor: '#6B7280',
headerShown: false,
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color, size }) => (
<Ionicons name="home" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="search"
options={{
title: 'Search',
tabBarIcon: ({ color, size }) => (
<Ionicons name="search" size={size} color={color} />
),
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
tabBarIcon: ({ color, size }) => (
<Ionicons name="person" size={size} color={color} />
),
}}
/>
</Tabs>
);
}
// app/user/[id].tsx
import { useLocalSearchParams } from 'expo-router';
import { View, Text } from 'react-native';
export default function UserPage(): React.ReactElement {
const { id } = useLocalSearchParams<{ id: string }>();
return (
<View className="flex-1 items-center justify-center">
<Text className="text-lg">User ID: {id}</Text>
</View>
);
}
// app/[category]/[id].tsx
import { useLocalSearchParams } from 'expo-router';
export default function ProductPage(): React.ReactElement {
const { category, id } = useLocalSearchParams<{
category: string;
id: string;
}>();
return (
<View>
<Text>Category: {category}</Text>
<Text>Product ID: {id}</Text>
</View>
);
}
// app/[...path].tsx
import { useLocalSearchParams } from 'expo-router';
export default function CatchAllPage(): React.ReactElement {
const { path } = useLocalSearchParams<{ path: string[] }>();
return (
<View>
<Text>Path segments: {path?.join('/')}</Text>
</View>
);
}
// app/_layout.tsx
import { Redirect, Stack } from 'expo-router';
import { useAuth } from '@/hooks/useAuth';
import { Loading } from '@/components/Loading';
export default function RootLayout(): React.ReactElement {
const { user, isLoading } = useAuth();
if (isLoading) {
return <Loading />;
}
if (!user) {
return <Redirect href="/login" />;
}
return <Stack />;
}
app/
├── (auth)/ # Auth group (no /auth in URL)
│ ├── login.tsx # /login
│ └── register.tsx # /register
├── (app)/ # App group (no /app in URL)
│ ├── home.tsx # /home
│ └── profile.tsx # /profile
// app/_layout.tsx
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen
name="modal"
options={{
presentation: 'modal',
animation: 'slide_from_bottom',
}}
/>
</Stack>
// Navigate to modal
router.push('/modal');
// Close modal
router.back();
// app.json
{
"expo": {
"scheme": "myapp",
"web": {
"bundler": "metro"
}
}
}
myapp:// # Opens app
myapp://profile # Opens /profile
myapp://user/123 # Opens /user/123
https://myapp.com/profile # Universal link
import { usePathname } from 'expo-router';
function Component(): React.ReactElement {
const pathname = usePathname();
// Returns: "/user/123"
return <Text>Current path: {pathname}</Text>;
}
import { useSegments } from 'expo-router';
function Component(): React.ReactElement {
const segments = useSegments();
// Returns: ["user", "123"]
return <Text>Segments: {segments.join(', ')}</Text>;
}
import { useFocusEffect } from 'expo-router';
import { useCallback } from 'react';
function Screen(): React.ReactElement {
useFocusEffect(
useCallback(() => {
// Runs when screen is focused
console.log('Screen focused');
return () => {
// Cleanup when screen loses focus
console.log('Screen unfocused');
};
}, [])
);
return <View />;
}
(name) to organize without affecting URLs_layout.tsx defines the navigation structure+not-found.tsx handles 404 routes