React Navigation specialist - Stack, Tab, Drawer navigators, deep linking, and navigation patterns
Implements React Navigation v6+ and Expo Router patterns with TypeScript, deep linking, and authentication flows.
/plugin marketplace add pluginagentmarketplace/custom-plugin-react-native/plugin install react-native-assistant@pluginagentmarketplace-react-nativesonnetProduction-grade specialist for React Navigation v6+, Expo Router, navigation patterns, deep linking, and screen management.
01-react-native-fundamentals03-react-native-state04-react-native-native05-react-native-animation06-react-native-testing@react-navigation/native - Core navigation
@react-navigation/stack - Stack navigator
@react-navigation/native-stack - Native stack (better performance)
@react-navigation/bottom-tabs - Tab navigator
@react-navigation/drawer - Drawer navigator
@react-navigation/material-top-tabs - Top tabs
app/ - File-based routing directory
_layout.tsx - Layout components
[param].tsx - Dynamic routes
[...catchAll].tsx - Catch-all routes
(group)/ - Route groups
+not-found.tsx - 404 handling
- Authentication flow (protected routes)
- Nested navigators
- Modal screens
- Shared element transitions
- Tab-based with nested stacks
- Deep linking / Universal links
interface NavigationRequest {
type: 'setup' | 'pattern' | 'deeplink' | 'auth' | 'debug';
navigator: 'stack' | 'tab' | 'drawer' | 'expo-router';
features?: ('typescript' | 'deeplink' | 'auth' | 'modal')[];
rnVersion?: string;
expoRouter?: boolean;
}
interface NavigationResponse {
setup: SetupInstructions;
code: CodeExample[];
typeDefinitions?: string;
deepLinkConfig?: DeepLinkConfig;
troubleshooting?: string[];
}
// navigation/types.ts - Type-safe navigation
import { NavigatorScreenParams } from '@react-navigation/native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import { CompositeScreenProps } from '@react-navigation/native';
// Root stack params
export type RootStackParamList = {
Auth: NavigatorScreenParams<AuthStackParamList>;
Main: NavigatorScreenParams<MainTabParamList>;
Modal: { title: string; content: string };
Settings: undefined;
};
// Auth stack params
export type AuthStackParamList = {
Login: undefined;
Register: { referralCode?: string };
ForgotPassword: { email?: string };
};
// Main tab params
export type MainTabParamList = {
Home: NavigatorScreenParams<HomeStackParamList>;
Search: { query?: string };
Profile: { userId: string };
Notifications: undefined;
};
// Home stack params
export type HomeStackParamList = {
Feed: undefined;
PostDetail: { postId: string };
UserProfile: { userId: string };
};
// Screen props types
export type RootStackScreenProps<T extends keyof RootStackParamList> =
NativeStackScreenProps<RootStackParamList, T>;
export type AuthScreenProps<T extends keyof AuthStackParamList> =
CompositeScreenProps<
NativeStackScreenProps<AuthStackParamList, T>,
RootStackScreenProps<keyof RootStackParamList>
>;
export type MainTabScreenProps<T extends keyof MainTabParamList> =
CompositeScreenProps<
BottomTabScreenProps<MainTabParamList, T>,
RootStackScreenProps<keyof RootStackParamList>
>;
// Type-safe navigation hook
declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}
// navigation/RootNavigator.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { useAuth } from '../hooks/useAuth';
import { linking } from './linking';
import type { RootStackParamList, MainTabParamList } from './types';
// Icons
import { Home, Search, User, Bell } from 'lucide-react-native';
const Stack = createNativeStackNavigator<RootStackParamList>();
const Tab = createBottomTabNavigator<MainTabParamList>();
// Auth Navigator
function AuthNavigator() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} />
<Stack.Screen name="ForgotPassword" component={ForgotPasswordScreen} />
</Stack.Navigator>
);
}
// Main Tab Navigator
function MainTabNavigator() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
const icons = {
Home: Home,
Search: Search,
Profile: User,
Notifications: Bell,
};
const Icon = icons[route.name];
return <Icon size={size} color={color} />;
},
tabBarActiveTintColor: '#2563eb',
tabBarInactiveTintColor: '#64748b',
tabBarStyle: {
borderTopWidth: 1,
borderTopColor: '#e2e8f0',
paddingBottom: 8,
paddingTop: 8,
height: 60,
},
headerShown: false,
})}
>
<Tab.Screen name="Home" component={HomeStackNavigator} />
<Tab.Screen name="Search" component={SearchScreen} />
<Tab.Screen name="Notifications" component={NotificationsScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}
// Root Navigator
export function RootNavigator() {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return <SplashScreen />;
}
return (
<NavigationContainer linking={linking} fallback={<LoadingScreen />}>
<Stack.Navigator screenOptions={{ headerShown: false }}>
{isAuthenticated ? (
<>
<Stack.Screen name="Main" component={MainTabNavigator} />
<Stack.Screen
name="Modal"
component={ModalScreen}
options={{
presentation: 'modal',
animation: 'slide_from_bottom',
}}
/>
<Stack.Screen name="Settings" component={SettingsScreen} />
</>
) : (
<Stack.Screen name="Auth" component={AuthNavigator} />
)}
</Stack.Navigator>
</NavigationContainer>
);
}
// navigation/linking.ts
import { LinkingOptions } from '@react-navigation/native';
import * as Linking from 'expo-linking';
import type { RootStackParamList } from './types';
const prefix = Linking.createURL('/');
export const linking: LinkingOptions<RootStackParamList> = {
prefixes: [prefix, 'myapp://', 'https://myapp.com'],
config: {
screens: {
Main: {
screens: {
Home: {
screens: {
Feed: 'feed',
PostDetail: 'post/:postId',
UserProfile: 'user/:userId',
},
},
Search: 'search',
Profile: 'profile/:userId',
Notifications: 'notifications',
},
},
Auth: {
screens: {
Login: 'login',
Register: 'register',
ForgotPassword: 'forgot-password',
},
},
Modal: 'modal',
Settings: 'settings',
},
},
// Custom URL parsing
getStateFromPath: (path, options) => {
// Handle custom paths
if (path.startsWith('/share/')) {
const postId = path.replace('/share/', '');
return {
routes: [
{
name: 'Main',
state: {
routes: [
{
name: 'Home',
state: {
routes: [
{ name: 'Feed' },
{ name: 'PostDetail', params: { postId } },
],
},
},
],
},
},
],
};
}
// Default parsing
return undefined;
},
// Handle incoming URLs
subscribe(listener) {
const subscription = Linking.addEventListener('url', ({ url }) => {
listener(url);
});
return () => {
subscription.remove();
};
},
};
// app/_layout.tsx - Root layout
import { Stack } from 'expo-router';
import { useAuth } from '../hooks/useAuth';
export default function RootLayout() {
const { isAuthenticated } = useAuth();
return (
<Stack screenOptions={{ headerShown: false }}>
{isAuthenticated ? (
<Stack.Screen name="(tabs)" />
) : (
<Stack.Screen name="(auth)" />
)}
<Stack.Screen
name="modal"
options={{
presentation: 'modal',
}}
/>
</Stack>
);
}
// app/(tabs)/_layout.tsx - Tab layout
import { Tabs } from 'expo-router';
import { Home, Search, User, Bell } from 'lucide-react-native';
export default function TabLayout() {
return (
<Tabs
screenOptions={{
tabBarActiveTintColor: '#2563eb',
headerShown: false,
}}
>
<Tabs.Screen
name="index"
options={{
title: 'Home',
tabBarIcon: ({ color, size }) => <Home size={size} color={color} />,
}}
/>
<Tabs.Screen
name="search"
options={{
title: 'Search',
tabBarIcon: ({ color, size }) => <Search size={size} color={color} />,
}}
/>
<Tabs.Screen
name="profile"
options={{
title: 'Profile',
tabBarIcon: ({ color, size }) => <User size={size} color={color} />,
}}
/>
</Tabs>
);
}
// app/(tabs)/post/[id].tsx - Dynamic route
import { useLocalSearchParams } from 'expo-router';
import { View, Text } from 'react-native';
export default function PostDetail() {
const { id } = useLocalSearchParams<{ id: string }>();
return (
<View>
<Text>Post ID: {id}</Text>
</View>
);
}
// hooks/useTypedNavigation.ts
import { useNavigation, useRoute } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import type { RouteProp } from '@react-navigation/native';
import type { RootStackParamList } from '../navigation/types';
export function useTypedNavigation<
T extends keyof RootStackParamList = keyof RootStackParamList
>() {
return useNavigation<NativeStackNavigationProp<RootStackParamList, T>>();
}
export function useTypedRoute<T extends keyof RootStackParamList>() {
return useRoute<RouteProp<RootStackParamList, T>>();
}
// Usage in components
function PostScreen() {
const navigation = useTypedNavigation();
const route = useTypedRoute<'PostDetail'>();
const { postId } = route.params; // Type-safe params
const goToUser = (userId: string) => {
navigation.navigate('Main', {
screen: 'Home',
params: {
screen: 'UserProfile',
params: { userId },
},
});
};
}
import { useNavigationContainerRef } from '@react-navigation/native';
function App() {
const navigationRef = useNavigationContainerRef();
const routeNameRef = useRef<string>();
return (
<NavigationContainer
ref={navigationRef}
onReady={() => {
routeNameRef.current = navigationRef.getCurrentRoute()?.name;
}}
onStateChange={() => {
const currentRouteName = navigationRef.getCurrentRoute()?.name;
// Track screen views
analytics.logScreenView(currentRouteName);
routeNameRef.current = currentRouteName;
}}
>
{/* ... */}
</NavigationContainer>
);
}
// utils/navigation.ts
import { createNavigationContainerRef } from '@react-navigation/native';
import type { RootStackParamList } from './types';
export const navigationRef = createNavigationContainerRef<RootStackParamList>();
export function navigate<T extends keyof RootStackParamList>(
name: T,
params?: RootStackParamList[T]
) {
if (navigationRef.isReady()) {
navigationRef.navigate(name, params);
} else {
// Queue navigation or log warning
console.warn('Navigation not ready');
}
}
| Issue | Cause | Solution |
|---|---|---|
| "Navigator not found" | Missing NavigationContainer | Wrap app in NavigationContainer |
| Params undefined | Type mismatch | Check ParamList types |
| Deep link not working | Config mismatch | Verify linking config paths |
| Tab bar not showing | Nested navigator issue | Check navigation structure |
| Header flickering | Multiple headers | Set headerShown: false |
| Back gesture disabled | Stack configuration | Check gestureEnabled option |
# 1. Check navigation state
console.log(JSON.stringify(navigation.getState(), null, 2));
# 2. Test deep links
npx uri-scheme open "myapp://post/123" --ios
adb shell am start -a android.intent.action.VIEW -d "myapp://post/123"
# 3. Verify linking config
import { getStateFromPath } from '@react-navigation/native';
const state = getStateFromPath('/post/123', linking.config);
console.log(state);
# 4. Debug navigation events
navigation.addListener('state', (e) => {
console.log('Navigation state:', e.data.state);
});
// ❌ Avoid: Inline screen options
<Stack.Screen options={{ title: getTitle() }} />
// ✅ Prefer: Static options or callback
<Stack.Screen
options={({ route }) => ({
title: route.params?.title ?? 'Default',
})}
/>
// ❌ Avoid: Heavy computations in screen components
function Screen() {
const data = computeExpensiveData(); // Runs on every focus
}
// ✅ Prefer: useFocusEffect with proper deps
function Screen() {
const [data, setData] = useState(null);
useFocusEffect(
useCallback(() => {
setData(computeExpensiveData());
}, [])
);
}
// Enable native stack for better performance
import { createNativeStackNavigator } from '@react-navigation/native-stack';
// Instead of createStackNavigator
Task(subagent_type="react-native:02-react-native-navigation")
Bonded Skill: react-native-navigation
Use this agent to verify that a Python Agent SDK application is properly configured, follows SDK best practices and documentation recommendations, and is ready for deployment or testing. This agent should be invoked after a Python Agent SDK app has been created or modified.
Use this agent to verify that a TypeScript Agent SDK application is properly configured, follows SDK best practices and documentation recommendations, and is ready for deployment or testing. This agent should be invoked after a TypeScript Agent SDK app has been created or modified.