skills/rn-navigation/SKILL.md
Expo Router navigation patterns for React Native. Use when implementing navigation, routing, deep links, tab bars, modals, or handling navigation state in Expo apps.
npx skillsauth add cjharmath/claude-agents-skills rn-navigationInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Expo Router uses file-system based routing. Files in app/ become routes.
app/
├── _layout.tsx # Root layout (providers, global UI)
├── index.tsx # "/" route
├── (tabs)/ # Tab group (parentheses = layout group)
│ ├── _layout.tsx # Tab bar configuration
│ ├── home.tsx # "/home" tab
│ └── profile.tsx # "/profile" tab
├── (auth)/ # Auth flow group
│ ├── _layout.tsx # Auth-specific layout
│ ├── login.tsx # "/login"
│ └── register.tsx # "/register"
├── settings/
│ ├── index.tsx # "/settings"
│ └── [id].tsx # "/settings/123" (dynamic)
└── [...missing].tsx # Catch-all 404
(groupName)Parentheses create layout groups - they affect layout hierarchy but not URL:
// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
export default function TabLayout() {
return (
<Tabs>
<Tabs.Screen
name="home"
options={{
title: 'Home',
tabBarIcon: ({ color }) => <HomeIcon color={color} />,
}}
/>
<Tabs.Screen
name="profile"
options={{ title: 'Profile' }}
/>
</Tabs>
);
}
[param]// app/player/[id].tsx
import { useLocalSearchParams } from 'expo-router';
export default function PlayerScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
return <PlayerProfile playerId={id} />;
}
[...slug]// app/[...missing].tsx
import { Link, Stack } from 'expo-router';
export default function NotFound() {
return (
<>
<Stack.Screen options={{ title: 'Not Found' }} />
<Link href="/">Go home</Link>
</>
);
}
import { useRouter, Link } from 'expo-router';
function MyComponent() {
const router = useRouter();
// Navigate with push (adds to stack)
router.push('/player/123');
// Navigate with params
router.push({
pathname: '/player/[id]',
params: { id: '123' },
});
// Replace current screen (no back)
router.replace('/home');
// Go back
router.back();
// Navigate to root
router.navigate('/');
// Dismiss modal
router.dismiss();
}
import { Link } from 'expo-router';
// Simple link
<Link href="/settings">Settings</Link>
// With params
<Link href={{ pathname: '/player/[id]', params: { id: '123' } }}>
View Player
</Link>
// As button
<Link href="/schedule" asChild>
<Pressable>
<Text>View Schedule</Text>
</Pressable>
</Link>
// Replace instead of push
<Link href="/home" replace>Home</Link>
// app/_layout.tsx
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' }}
/>
<Stack.Screen
name="player/[id]"
options={{
headerTitle: 'Player',
headerBackTitle: 'Back',
}}
/>
</Stack>
);
}
// app/player/[id].tsx
import { Stack, useLocalSearchParams } from 'expo-router';
export default function PlayerScreen() {
const { id } = useLocalSearchParams();
const player = usePlayer(id);
return (
<>
<Stack.Screen
options={{
headerTitle: player?.name ?? 'Loading...',
headerRight: () => (
<EditButton playerId={id} />
),
}}
/>
<PlayerProfile player={player} />
</>
);
}
// Present as modal from anywhere
router.push('/booking-modal');
// app/booking-modal.tsx
import { Stack, useRouter } from 'expo-router';
export default function BookingModal() {
const router = useRouter();
const handleComplete = () => {
router.dismiss(); // or router.back()
};
return (
<>
<Stack.Screen
options={{
presentation: 'modal',
headerLeft: () => (
<Button title="Cancel" onPress={() => router.dismiss()} />
),
}}
/>
<BookingForm onComplete={handleComplete} />
</>
);
}
// In _layout.tsx, configure the modal screen
<Stack.Screen
name="booking-modal"
options={{
presentation: 'modal',
headerShown: true,
}}
/>
{
"expo": {
"scheme": "myapp",
"ios": {
"bundleIdentifier": "com.yourcompany.myapp",
"associatedDomains": ["applinks:yourdomain.com"]
}
}
}
# iOS Simulator
npx uri-scheme open "myapp://player/123" --ios
# Physical device
npx expo start --dev-client
# Then open myapp://player/123 in Safari
associatedDomains to app.jsonapple-app-site-association file at https://yourdomain.com/.well-known/apple-app-site-association:{
"applinks": {
"apps": [],
"details": [{
"appID": "TEAMID.com.yourcompany.myapp",
"paths": ["/player/*", "/schedule/*"]
}]
}
}
// app/_layout.tsx
import { useEffect } from 'react';
import * as Linking from 'expo-linking';
import { useRouter } from 'expo-router';
export default function RootLayout() {
const router = useRouter();
useEffect(() => {
// Handle link that opened the app
Linking.getInitialURL().then((url) => {
if (url) handleDeepLink(url);
});
// Handle links while app is open
const subscription = Linking.addEventListener('url', ({ url }) => {
handleDeepLink(url);
});
return () => subscription.remove();
}, []);
function handleDeepLink(url: string) {
const { path, queryParams } = Linking.parse(url);
// Expo Router handles most cases automatically
// Custom logic here for special cases
}
return <Stack />;
}
See rn-auth skill for full auth context pattern. Key navigation piece:
// app/_layout.tsx
export default function RootLayout() {
const { token, isLoading } = useAuth();
const segments = useSegments();
const router = useRouter();
useEffect(() => {
if (isLoading) return;
const inAuthGroup = segments[0] === '(auth)';
if (!token && !inAuthGroup) {
router.replace('/(auth)/login');
} else if (token && inAuthGroup) {
router.replace('/(tabs)/home');
}
}, [token, isLoading]);
return (
<Stack>
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
</Stack>
);
}
// After login success, replace to prevent back to login
router.replace('/(tabs)/home');
// For onboarding completion
router.replace('/home');
// In screen options
<Stack.Screen
name="checkout-complete"
options={{
headerBackVisible: false,
gestureEnabled: false, // Prevent swipe back
}}
/>
// Option 1: URL params (simple data, survives refresh)
router.push({
pathname: '/confirm',
params: { date: '2025-01-15', courtId: '5' },
});
// Reading
const { date, courtId } = useLocalSearchParams();
// Option 2: Global state for complex data (doesn't survive refresh)
// Use context, zustand, or similar
import { usePathname, useSegments } from 'expo-router';
function DebugNav() {
const pathname = usePathname();
const segments = useSegments();
console.log('Current path:', pathname);
console.log('Segments:', segments);
return null;
}
| Issue | Solution |
|-------|----------|
| Screen not found | Check file name matches route, check _layout.tsx includes screen |
| Tabs not showing | Ensure tab screens are direct children of tab _layout.tsx |
| Back button missing | Check headerShown in parent and child layouts |
| Deep link not working | Verify scheme in app.json, test with uri-scheme CLI |
| Params undefined | Use useLocalSearchParams not useSearchParams |
development
Styling patterns for React web applications. Use when working with Tailwind CSS, CSS Modules, theming, responsive design, or component styling.
development
Navigation and routing patterns for React web applications. Use when implementing React Router, Next.js routing, deep links, or handling navigation state.
development
Building and deploying React web applications. Use when configuring builds, deploying to Vercel/Netlify, setting up CI/CD, Docker, or managing environments.
development
Authentication patterns for React web applications. Use when implementing login flows, OAuth, JWT handling, session management, or protected routes in React web apps.