Implementing multi-screen navigation in React Native apps
Scope: React Navigation library, stack/tab/drawer navigation, deep linking, iOS-specific patterns Lines: ~340 Last Updated: 2025-10-18
Activate this skill when:
Navigator Types:
Navigation Container:
Platform Patterns:
Gesture Handling:
Navigation Params:
Navigation Lifecycle:
// types/navigation.ts
import { NavigationProp, RouteProp } from '@react-navigation/native';
// Define root stack param list
export type RootStackParamList = {
Home: undefined;
Profile: { userId: string };
Settings: undefined;
Post: { postId: string; title?: string };
};
// Define tab param list
export type TabParamList = {
Feed: undefined;
Search: undefined;
Notifications: undefined;
Profile: undefined;
};
// Helper types for screen props
export type RootStackNavigation = NavigationProp<RootStackParamList>;
export type ProfileRouteProp = RouteProp<RootStackParamList, 'Profile'>;
// Declare global navigation type
declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}
Benefits:
// App.tsx
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { RootStackParamList } from './types/navigation';
const Stack = createNativeStackNavigator<RootStackParamList>();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator
screenOptions={{
headerLargeTitle: true, // iOS large titles
headerTransparent: false,
headerBlurEffect: 'regular', // iOS blur
animation: 'default', // Native iOS animations
gestureEnabled: true, // Swipe back gesture
fullScreenGestureEnabled: true,
}}
>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: 'Home' }}
/>
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={({ route }) => ({
title: `User ${route.params.userId}`,
headerBackTitle: 'Back', // iOS custom back text
})}
/>
<Stack.Screen
name="Settings"
component={SettingsScreen}
options={{
presentation: 'modal', // iOS modal presentation
headerLargeTitle: false,
}}
/>
</Stack.Navigator>
</NavigationContainer>
);
}
When to use:
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
const Tab = createBottomTabNavigator<TabParamList>();
function TabNavigator() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
const icons: Record<string, string> = {
Feed: focused ? 'home' : 'home-outline',
Search: focused ? 'search' : 'search-outline',
Notifications: focused ? 'notifications' : 'notifications-outline',
Profile: focused ? 'person' : 'person-outline',
};
return <Ionicons name={icons[route.name]} size={size} color={color} />;
},
tabBarActiveTintColor: '#007AFF', // iOS blue
tabBarInactiveTintColor: '#8E8E93', // iOS gray
tabBarStyle: {
backgroundColor: '#F2F2F7', // iOS tab bar background
borderTopWidth: 0,
elevation: 0,
},
headerShown: false, // Headers in nested stacks
})}
>
<Tab.Screen name="Feed" component={FeedStack} />
<Tab.Screen name="Search" component={SearchStack} />
<Tab.Screen
name="Notifications"
component={NotificationsStack}
options={{
tabBarBadge: 3, // iOS badge
}}
/>
<Tab.Screen name="Profile" component={ProfileStack} />
</Tab.Navigator>
);
}
Benefits:
import { useNavigation, useRoute } from '@react-navigation/native';
import { RootStackNavigation, ProfileRouteProp } from '../types/navigation';
function ProfileScreen() {
const navigation = useNavigation<RootStackNavigation>();
const route = useRoute<ProfileRouteProp>();
const { userId } = route.params;
const handleEditProfile = () => {
navigation.navigate('Settings');
};
const handleGoBack = () => {
navigation.goBack();
};
const handleViewPost = (postId: string) => {
navigation.navigate('Post', { postId, title: 'My Post' });
};
return (
<View>
<Text>Profile: {userId}</Text>
<Button title="Edit Profile" onPress={handleEditProfile} />
<Button title="Go Back" onPress={handleGoBack} />
</View>
);
}
When to use:
import { LinkingOptions } from '@react-navigation/native';
import * as Linking from 'expo-linking';
const linking: LinkingOptions<RootStackParamList> = {
prefixes: [
'myapp://', // Custom URL scheme
'https://myapp.com', // Universal links
],
config: {
screens: {
Home: '',
Profile: 'user/:userId',
Post: {
path: 'post/:postId',
parse: {
postId: (id) => id, // Custom parsing
},
},
Settings: 'settings',
},
},
async getInitialURL() {
// Check if app was opened from deep link
const url = await Linking.getInitialURL();
if (url != null) {
return url;
}
// Handle push notifications
// const notification = await getInitialNotification();
// return notification?.data?.url;
},
subscribe(listener) {
// Listen for deep links while app is running
const subscription = Linking.addEventListener('url', ({ url }) => {
listener(url);
});
return () => subscription.remove();
},
};
// In NavigationContainer
<NavigationContainer linking={linking}>
{/* navigators */}
</NavigationContainer>
Benefits:
import { createNativeStackNavigator } from '@react-native-stack-navigator';
type AuthStackParamList = {
Login: undefined;
SignUp: undefined;
ForgotPassword: undefined;
};
type AppStackParamList = {
Main: undefined;
Profile: { userId: string };
};
const AuthStack = createNativeStackNavigator<AuthStackParamList>();
const AppStack = createNativeStackNavigator<AppStackParamList>();
function AuthNavigator() {
return (
<AuthStack.Navigator
screenOptions={{
headerShown: false,
presentation: 'modal',
}}
>
<AuthStack.Screen name="Login" component={LoginScreen} />
<AuthStack.Screen name="SignUp" component={SignUpScreen} />
<AuthStack.Screen name="ForgotPassword" component={ForgotPasswordScreen} />
</AuthStack.Navigator>
);
}
function AppNavigator() {
return (
<AppStack.Navigator>
<AppStack.Screen name="Main" component={TabNavigator} />
<AppStack.Screen name="Profile" component={ProfileScreen} />
</AppStack.Navigator>
);
}
// Root navigator with auth state
export default function RootNavigator() {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return <SplashScreen />;
}
return (
<NavigationContainer>
{isAuthenticated ? <AppNavigator /> : <AuthNavigator />}
</NavigationContainer>
);
}
When to use:
function RootNavigator() {
return (
<Stack.Navigator>
{/* Main app screens */}
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
{/* Modal screens group */}
<Stack.Group
screenOptions={{
presentation: 'modal',
headerShown: true,
headerLeft: () => (
<Button title="Close" onPress={() => navigation.goBack()} />
),
}}
>
<Stack.Screen name="CreatePost" component={CreatePostScreen} />
<Stack.Screen name="ShareSheet" component={ShareSheetScreen} />
</Stack.Group>
{/* Full screen modal */}
<Stack.Screen
name="ImageViewer"
component={ImageViewerScreen}
options={{
presentation: 'fullScreenModal',
headerShown: false,
animation: 'fade',
}}
/>
</Stack.Navigator>
);
}
Benefits:
function PostScreen() {
const navigation = useNavigation();
React.useLayoutEffect(() => {
navigation.setOptions({
headerRight: () => (
<Button
title="Share"
onPress={() => navigation.navigate('ShareSheet')}
/>
),
headerBackTitle: 'Posts', // iOS custom back text
headerLargeTitle: false,
headerTransparent: false,
});
}, [navigation]);
return <View>{/* content */}</View>;
}
// Or in navigator options
<Stack.Screen
name="Post"
component={PostScreen}
options={({ navigation, route }) => ({
title: route.params.title ?? 'Post',
headerRight: () => (
<Pressable onPress={() => console.log('Share')}>
<Ionicons name="share-outline" size={24} color="#007AFF" />
</Pressable>
),
})}
/>
When to use:
Method | Purpose | Example
------------------------------------------|----------------------------------|------------------
navigation.navigate('Screen', params) | Navigate to screen | Navigate with params
navigation.push('Screen', params) | Push new instance on stack | Allow duplicates
navigation.goBack() | Go back one screen | Dismiss/pop
navigation.pop() | Pop from stack | Same as goBack
navigation.popToTop() | Pop to first screen in stack | Reset stack
navigation.replace('Screen', params) | Replace current screen | Auth flow
navigation.reset({ routes: [...] }) | Reset entire navigation state | Deep state change
{
// Header
headerShown: true,
headerTitle: 'Title',
headerLargeTitle: true,
headerTransparent: false,
headerBlurEffect: 'regular',
headerBackTitle: 'Back',
// Presentation
presentation: 'card' | 'modal' | 'fullScreenModal',
animation: 'default' | 'fade' | 'slide_from_bottom',
// Gestures
gestureEnabled: true,
fullScreenGestureEnabled: true,
gestureDirection: 'horizontal' | 'vertical',
// Status bar
statusBarStyle: 'auto' | 'dark' | 'light',
statusBarAnimation: 'fade' | 'slide',
}
✅ DO: Use TypeScript for type-safe navigation
✅ DO: Use native stack for iOS-specific apps
✅ DO: Configure deep linking for all screens
✅ DO: Keep navigation state shallow (avoid deep nesting)
✅ DO: Use modals for temporary actions
❌ DON'T: Nest more than 2-3 levels of navigators
❌ DON'T: Pass large objects in route params
❌ DON'T: Ignore deep linking configuration
❌ DON'T: Use stack navigator for everything (tabs exist)
❌ DON'T: Store navigation state in React state
❌ Deep navigator nesting: Stack → Tabs → Stack → Stack (4+ levels) ✅ Keep nesting to 2-3 levels max, use modal presentation for edge cases
❌ Passing functions in params: navigation.navigate('Screen', { onSave: () => {} }) ✅ Use navigation events or state management (Context, Redux)
❌ Prop drilling navigation: Passing navigation prop through many components ✅ Use useNavigation() hook in any component
❌ Ignoring TypeScript types: Using any for navigation ✅ Define and use typed param lists for all navigators
❌ Not handling deep links: App doesn't respond to URLs ✅ Configure linking for all screens, test with URL schemes
❌ Storing navigation reference globally: let navRef; navRef = navigation; ✅ Use navigationRef from @react-navigation/native with TypeScript
❌ Rebuilding entire navigation on state change: Auth state toggles navigator ✅ Use conditional rendering at root level, not deep in tree
❌ Not using native stack on iOS: Using regular stack for iOS-only apps ✅ Use @react-navigation/native-stack for better performance and native feel
react-native-setup.md - Project initialization and dependenciesreact-native-performance.md - Navigation performance optimizationreact-native-native-modules.md - Custom native navigation componentsswiftui-navigation.md - iOS native navigation patternsreact-component-patterns.md - Component composition with navigationfrontend-state-management.md - Managing app state with navigationLast Updated: 2025-10-18 Format Version: 1.0 (Atomic)