Claude-skill-registry expo-router-patterns
Expo Router file-based navigation patterns. Use when implementing navigation.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/expo-router-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-expo-router-patterns && rm -rf "$T"
manifest:
skills/data/expo-router-patterns/SKILL.mdsource content
Expo Router Patterns Skill
This skill covers Expo Router navigation for React Native.
When to Use
Use this skill when:
- Setting up navigation
- Creating screens and routes
- Implementing deep linking
- Protecting routes
Core Principle
FILE-BASED ROUTING - Routes are defined by file structure (like Next.js).
File Structure
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
Basic Navigation
Link Component
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>
useRouter Hook
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>; }
Layout Components
Root Layout
// 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> </> ); }
Tab Layout
// 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> ); }
Dynamic Routes
Single Parameter
// 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> ); }
Multiple Parameters
// 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> ); }
Catch-All Route
// 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> ); }
Protected Routes
// 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 />; }
Route Groups
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
Modal Routes
// 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();
Deep Linking
Configuration
// app.json { "expo": { "scheme": "myapp", "web": { "bundler": "metro" } } }
Links
myapp:// # Opens app myapp://profile # Opens /profile myapp://user/123 # Opens /user/123 https://myapp.com/profile # Universal link
Navigation Hooks
usePathname
import { usePathname } from 'expo-router'; function Component(): React.ReactElement { const pathname = usePathname(); // Returns: "/user/123" return <Text>Current path: {pathname}</Text>; }
useSegments
import { useSegments } from 'expo-router'; function Component(): React.ReactElement { const segments = useSegments(); // Returns: ["user", "123"] return <Text>Segments: {segments.join(', ')}</Text>; }
useFocusEffect
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 />; }
Notes
- Use route groups
to organize without affecting URLs(name) - Layouts cascade (parent layouts wrap children)
defines the navigation structure_layout.tsx
handles 404 routes+not-found.tsx- Deep linking is configured automatically
- Use typed params with generics for type safety