Stack Navigator – The Foundation of Mobile Navigation
Stack Navigator is the most common navigation pattern in mobile apps. It manages a stack of screens, pushing new screens onto the stack and popping to go back. React Navigation provides two stack implementations: createNativeStackNavigator (recommended, uses native UINavigationController on iOS and Fragment on Android) and createStackNavigator (JavaScript-based, for custom transitions). In 2026, NativeStackNavigator is the default choice for production apps.
- Installation & Setup
Native Stack Navigator requires the core navigation package and native dependencies. For Expo managed projects, use npx expo install. For bare React Native projects, manual linking may be required.
# Core packages npm install @react-navigation/native @react-navigation/native-stack # Peer dependencies (Expo managed) npx expo install react-native-screens react-native-safe-area-context # Peer dependencies (bare React Native) npm install react-native-screens react-native-safe-area-context cd ios && pod install && cd .. # For Android, add to MainActivity.java: # import android.os.Bundle; # @Override # protected void onCreate(Bundle savedInstanceState) { # super.onCreate(null); # }
- Basic Native Stack Navigator
Create a stack navigator with screens, configure options, and navigate between them using navigation methods.
// App.tsx - Basic Native Stack Navigator import * as React from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { Button, View, Text, StyleSheet, Alert } from 'react-native'; // Type definitions for navigation params type RootStackParamList = { Home: undefined; Details: { itemId: number; title: string }; Profile: { userId: string }; Modal: { message: string }; }; const Stack = createNativeStackNavigator<RootStackParamList>(); // Home Screen function HomeScreen({ navigation }: { navigation: any }) { return ( <View style={styles.container}> <Text style={styles.title}>Home Screen</Text> {/* Basic navigation with params */} <Button title="Go to Details" onPress={() => navigation.navigate('Details', { itemId: 42, title: 'The Answer' })} /> {/* Push always adds new screen */} <Button title="Push Details (always new)" onPress={() => navigation.push('Details', { itemId: Date.now(), title: 'New Instance' })} /> {/* Replace current screen */} <Button title="Replace with Profile" onPress={() => navigation.replace('Profile', { userId: 'new_user' })} /> {/* Go back */} <Button title="Go Back" onPress={() => navigation.goBack()} /> {/* Pop to first screen */} <Button title="Pop to Top" onPress={() => navigation.popToTop()} /> </View> ); } // Details Screen with params function DetailsScreen({ route, navigation }: { route: any; navigation: any }) { const { itemId, title } = route.params; return ( <View style={styles.container}> <Text style={styles.title}>Details Screen</Text> <Text>Item ID: {itemId}</Text> <Text>Title: {title}</Text> {/* Update current screen params */} <Button title="Update Title" onPress={() => navigation.setParams({ title: 'Updated Title' })} /> {/* Navigate to parent */} <Button title="Go to Home" onPress={() => navigation.popToTop()} /> </View> ); } // Profile Screen function ProfileScreen({ route, navigation }: { route: any; navigation: any }) { const { userId } = route.params; // Navigate and replace current screen return ( <View style={styles.container}> <Text style={styles.title}>Profile</Text> <Text>User ID: {userId}</Text> <Button title="Edit Profile" onPress={() => navigation.navigate('Modal', { message: 'Edit your profile' })} /> </View> ); } // Modal Screen (different presentation) function ModalScreen({ route, navigation }: { route: any; navigation: any }) { return ( <View style={styles.modalContainer}> <Text style={styles.title}>Modal</Text> <Text>{route.params.message}</Text> <Button title="Close" onPress={() => navigation.goBack()} /> </View> ); } export default function App() { return ( <NavigationContainer> <Stack.Navigator initialRouteName="Home" screenOptions={{ headerStyle: { backgroundColor: '#007AFF' }, headerTintColor: '#FFFFFF', headerTitleStyle: { fontWeight: 'bold' }, headerBackTitle: 'Back', gestureEnabled: true, // iOS swipe back animation: 'slide_from_right', // default animation }} > <Stack.Screen name="Home" component={HomeScreen} options={{ title: 'Welcome' }} /> <Stack.Screen name="Details" component={DetailsScreen} options={({ route }) => ({ title: route.params.title })} /> <Stack.Screen name="Profile" component={ProfileScreen} options={{ title: 'User Profile' }} /> {/* Modal presentation */} <Stack.Screen name="Modal" component={ModalScreen} options={{ presentation: 'modal', headerShown: false, }} /> </Stack.Navigator> </NavigationContainer> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16, padding: 16, }, title: { fontSize: 24, fontWeight: 'bold', marginBottom: 20, }, modalContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(0,0,0,0.9)', gap: 16, }, }); // Navigation Methods Reference: // navigation.navigate('RouteName', params) - Navigate to route, don't push if exists // navigation.push('RouteName', params) - Always push new screen // navigation.goBack() - Go back one screen // navigation.popToTop() - Go back to first screen in stack // navigation.replace('RouteName', params) - Replace current screen // navigation.setParams(params) - Update current screen params // navigation.canGoBack() - Check if can go back
- Header Customization
Native Stack Navigator provides extensive header customization options including custom components, buttons, large titles, and search bars.
// HeaderCustomization.tsx import * as React from 'react'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { Button, View, Text, TouchableOpacity, Platform } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; const Stack = createNativeStackNavigator(); function HomeScreen({ navigation }: { navigation: any }) { React.useLayoutEffect(() => { navigation.setOptions({ headerRight: () => ( <Button onPress={() => navigation.navigate('Settings')} title="Settings" color="#fff" /> ), headerLeft: () => ( <TouchableOpacity onPress={() => console.log('Menu pressed')} style={{ marginLeft: 16 }}> <Ionicons name="menu" size={24} color="#fff" /> </TouchableOpacity> ), }); }, [navigation]); return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>Home Screen</Text> </View> ); } function SettingsScreen() { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>Settings Screen</Text> </View> ); } export default function HeaderCustomizationApp() { return ( <NavigationContainer> <Stack.Navigator screenOptions={{ headerStyle: { backgroundColor: '#007AFF' }, headerTintColor: '#fff', headerTitleStyle: { fontWeight: 'bold', fontSize: 20 }, // iOS large title headerLargeTitle: Platform.OS === 'ios', headerLargeTitleStyle: { fontSize: 34, fontWeight: 'bold' }, // Header shadow headerShadowVisible: false, // Back button customization headerBackTitle: 'Back', headerBackTitleStyle: { fontSize: 16 }, headerBackVisible: true, // Hide header for specific screens // headerShown: false, }} > <Stack.Screen name="Home" component={HomeScreen} options={{ title: 'Dashboard', // Large title only on this screen headerLargeTitle: true, }} /> <Stack.Screen name="Settings" component={SettingsScreen} options={{ title: 'App Settings', presentation: 'card', }} /> </Stack.Navigator> </NavigationContainer> ); } // Custom header component function CustomHeader() { return ( <View style={{ flexDirection: 'row', alignItems: 'center', padding: 16 }}> <Text style={{ fontSize: 20, fontWeight: 'bold' }}>Custom Header</Text> </View> ); } // Usage in screen options: // header: (props) => <CustomHeader {...props} /> // headerTitle: (props) => <CustomTitle {...props} /> // headerBackground: () => <LinearGradient colors={['#007AFF', '#0055CC']} />
- Screen Transitions & Animations
Native Stack Navigator supports various screen transition animations including default, fade, flip, and custom animations on iOS.
// ScreenTransitions.tsx import * as React from 'react'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { Button, View, Text, StyleSheet } from 'react-native'; const Stack = createNativeStackNavigator(); function HomeScreen({ navigation }: { navigation: any }) { return ( <View style={styles.container}> <Text style={styles.title}>Home</Text> <Button title="Slide from Right" onPress={() => navigation.navigate('SlideScreen')} /> <Button title="Fade Animation" onPress={() => navigation.navigate('FadeScreen')} /> <Button title="Flip Animation" onPress={() => navigation.navigate('FlipScreen')} /> <Button title="Modal Presentation" onPress={() => navigation.navigate('ModalScreen')} /> </View> ); } function SlideScreen() { return ( <View style={styles.container}> <Text>Slide from Right (Default)</Text> </View> ); } function FadeScreen() { return ( <View style={styles.container}> <Text>Fade Animation</Text> </View> ); } function FlipScreen() { return ( <View style={styles.container}> <Text>Flip Horizontal (iOS only)</Text> </View> ); } function ModalContentScreen() { return ( <View style={styles.modalContainer}> <Text>Modal Presentation</Text> </View> ); } export default function TransitionsApp() { return ( <NavigationContainer> <Stack.Navigator initialRouteName="Home"> <Stack.Screen name="Home" component={HomeScreen} /> {/* Different animations */} <Stack.Screen name="SlideScreen" component={SlideScreen} options={{ animation: 'slide_from_right' }} /> <Stack.Screen name="FadeScreen" component={FadeScreen} options={{ animation: 'fade' }} /> <Stack.Screen name="FlipScreen" component={FlipScreen} options={{ animation: 'flip' }} /> {/* Presentation styles */} <Stack.Screen name="ModalScreen" component={ModalContentScreen} options={{ presentation: 'modal', animation: 'slide_from_bottom', headerShown: false, }} /> </Stack.Navigator> </NavigationContainer> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 }, title: { fontSize: 24, fontWeight: 'bold' }, modalContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(0,0,0,0.9)' }, }); // Animation Options: // animation: 'slide_from_right' (default) // animation: 'slide_from_left' // animation: 'slide_from_bottom' // animation: 'fade' // animation: 'flip' (iOS only) // animation: 'none' // // Presentation Options: // presentation: 'card' (default) // presentation: 'modal' (iOS modal style, slide from bottom) // presentation: 'transparentModal' (modal with transparent background) // presentation: 'fullScreenModal' (full screen modal) // presentation: 'formSheet' (iOS form sheet on iPad)
- Navigation Lifecycle & Hooks
React Navigation provides hooks to respond to screen focus, blur, and state changes. Use these for data fetching, analytics, and cleanup.
// NavigationLifecycle.tsx import * as React from 'react'; import { useFocusEffect, useIsFocused, useNavigation, useRoute, } from '@react-navigation/native'; import { View, Text, Button, ActivityIndicator } from 'react-native'; function DataFetchingScreen() { const [isLoading, setIsLoading] = React.useState(false); const [data, setData] = React.useState(null); // Method 1: useFocusEffect (recommended for data fetching) useFocusEffect( React.useCallback(() => { let isActive = true; const fetchData = async () => { setIsLoading(true); try { const result = await fetch('https://api.example.com/data'); const json = await result.json(); if (isActive) { setData(json); } } catch (error) { console.error(error); } finally { if (isActive) { setIsLoading(false); } } }; fetchData(); // Cleanup when screen loses focus return () => { isActive = false; console.log('Screen blurred - cleanup'); }; }, []) ); // Method 2: useIsFocused (triggers re-render) const isFocused = useIsFocused(); if (isLoading) { return <ActivityIndicator size="large" />; } return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>Screen Focused: {isFocused ? 'Yes' : 'No'}</Text> <Text>{JSON.stringify(data)}</Text> </View> ); } // BeforeRemove event (prevent accidental navigation) function FormScreen({ navigation }: { navigation: any }) { const [hasUnsavedChanges, setHasUnsavedChanges] = React.useState(false); React.useEffect(() => { const unsubscribe = navigation.addListener('beforeRemove', (e: any) => { if (!hasUnsavedChanges) { return; } e.preventDefault(); // Show confirmation dialog Alert.alert( 'Discard changes?', 'You have unsaved changes. Are you sure you want to leave?', [ { text: 'Stay', style: 'cancel', onPress: () => {} }, { text: 'Discard', style: 'destructive', onPress: () => navigation.dispatch(e.data.action), }, ] ); }); return unsubscribe; }, [navigation, hasUnsavedChanges]); return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>Form Screen</Text> <Button title="Edit" onPress={() => setHasUnsavedChanges(true)} /> </View> ); } // Tab press to scroll to top function ScrollableScreen() { const scrollRef = React.useRef(null); const navigation = useNavigation(); React.useEffect(() => { const unsubscribe = navigation.addListener('tabPress', (e: any) => { // Prevent default tab behavior // e.preventDefault(); // Scroll to top scrollRef.current?.scrollTo({ y: 0, animated: true }); }); return unsubscribe; }, [navigation]); return ( <ScrollView ref={scrollRef}> {[...Array(50)].map((_, i) => ( <Text key={i}>Item {i + 1}</Text> ))} </ScrollView> ); } // Navigation state tracking function NavigationTracker() { const navigation = useNavigation(); const route = useRoute(); React.useEffect(() => { // Track screen view for analytics console.log(`Screen viewed: ${route.name}`); // Track navigation state changes const unsubscribe = navigation.addListener('state', (e) => { console.log('Navigation state changed', e.data.state); }); return unsubscribe; }, [navigation, route]); return null; } // Lifecycle Events: // focus - Screen came into focus // blur - Screen went out of focus // beforeRemove - Screen is about to be removed // state - Navigation state changed // transitionStart - Transition animation started // transitionEnd - Transition animation ended
- Passing & Accessing Parameters
Pass data between screens using route params. TypeScript ensures type safety for parameters.
// ParamsExample.tsx - Full TypeScript implementation import * as React from 'react'; import { NavigationContainer, RouteProp, useNavigation, useRoute, } from '@react-navigation/native'; import { createNativeStackNavigator, NativeStackNavigationProp } from '@react-navigation/native-stack'; import { Button, View, Text, TextInput, StyleSheet } from 'react-native'; // Define param types for each screen type RootStackParamList = { Home: undefined; Product: { id: number; name: string }; Checkout: { items: Array<{ id: number; name: string; price: number }>; total: number; promoCode?: string; }; UserProfile: { userId: string; readonly?: boolean }; }; // Type-safe navigation and route hooks type HomeScreenNavigationProp = NativeStackNavigationProp<RootStackParamList, 'Home'>; type ProductScreenRouteProp = RouteProp<RootStackParamList, 'Product'>; type CheckoutScreenRouteProp = RouteProp<RootStackParamList, 'Checkout'>; const Stack = createNativeStackNavigator<RootStackParamList>(); function HomeScreen() { const navigation = useNavigation<HomeScreenNavigationProp>(); return ( <View style={styles.container}> <Text style={styles.title}>Products</Text> {/* Passing single object */} <Button title="View Product 1" onPress={() => navigation.navigate('Product', { id: 1, name: 'iPhone 15 Pro' })} /> {/* Passing complex data */} <Button title="Go to Checkout" onPress={() => navigation.navigate('Checkout', { items: [ { id: 1, name: 'iPhone', price: 999 }, { id: 2, name: 'Case', price: 49 }, ], total: 1048, promoCode: 'SAVE10', }) } /> </View> ); } function ProductScreen() { const route = useRoute<ProductScreenRouteProp>(); const navigation = useNavigation(); const { id, name } = route.params; // Update params const updateProduct = () => { navigation.setParams({ name: `${name} (Updated)` }); }; return ( <View style={styles.container}> <Text style={styles.title}>{name}</Text> <Text>Product ID: {id}</Text> <Button title="Update Name" onPress={updateProduct} /> </View> ); } function CheckoutScreen() { const route = useRoute<CheckoutScreenRouteProp>(); const { items, total, promoCode } = route.params; return ( <View style={styles.container}> <Text style={styles.title}>Checkout</Text> <Text>Items: {items.length}</Text> <Text>Total: ${total}</Text> {promoCode && <Text>Promo Code: {promoCode}</Text>} </View> ); } // Default params (when param is optional) function UserProfileScreen() { const route = useRoute<RouteProp<RootStackParamList, 'UserProfile'>>(); const { userId, readonly = false } = route.params; return ( <View style={styles.container}> <Text>User: {userId}</Text> <Text>Readonly: {readonly ? 'Yes' : 'No'}</Text> </View> ); } export default function ParamsApp() { return ( <NavigationContainer> <Stack.Navigator> <Stack.Screen name="Home" component={HomeScreen} /> <Stack.Screen name="Product" component={ProductScreen} /> <Stack.Screen name="Checkout" component={CheckoutScreen} /> <Stack.Screen name="UserProfile" component={UserProfileScreen} /> </Stack.Navigator> </NavigationContainer> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 }, title: { fontSize: 24, fontWeight: 'bold' }, }); // Parameter best practices: // 1. Only pass serializable data (avoid functions, class instances) // 2. Use TypeScript for type safety // 3. Keep params minimal - pass IDs, fetch full data in child screen // 4. Use optional params with default values // 5. Avoid passing entire objects when ID would suffice // 6. Use route.params?.field for optional params
- Nesting Stack Navigators
Nesting stack navigators inside tabs or drawers is common. Access parent navigators and pass params between nested stacks.
// NestedStacks.tsx - Stack inside Tab Navigator import * as React from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { Button, View, Text } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; // ----- Home Stack ----- type HomeStackParamList = { HomeList: undefined; HomeDetail: { id: number }; }; const HomeStack = createNativeStackNavigator<HomeStackParamList>(); function HomeListScreen({ navigation }: { navigation: any }) { return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 }}> <Text>Home List</Text> <Button title="Go to Detail" onPress={() => navigation.navigate('HomeDetail', { id: 42 })} /> {/* Access parent tab navigator */} <Button title="Open Drawer (if exists)" onPress={() => navigation.getParent()?.openDrawer?.()} /> </View> ); } function HomeDetailScreen({ route, navigation }: { route: any; navigation: any }) { const { id } = route.params; return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 }}> <Text>Home Detail - ID: {id}</Text> <Button title="Go Back" onPress={() => navigation.goBack()} /> {/* Navigate in parent tab */} <Button title="Go to Profile Tab" onPress={() => navigation.getParent()?.navigate('ProfileTab')} /> </View> ); } function HomeStackScreen() { return ( <HomeStack.Navigator> <HomeStack.Screen name="HomeList" component={HomeListScreen} /> <HomeStack.Screen name="HomeDetail" component={HomeDetailScreen} /> </HomeStack.Navigator> ); } // ----- Profile Stack ----- type ProfileStackParamList = { ProfileMain: undefined; ProfileEdit: undefined; }; const ProfileStack = createNativeStackNavigator<ProfileStackParamList>(); function ProfileMainScreen({ navigation }: { navigation: any }) { return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 }}> <Text>Profile Main</Text> <Button title="Edit Profile" onPress={() => navigation.navigate('ProfileEdit')} /> </View> ); } function ProfileEditScreen() { return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <Text>Edit Profile</Text> </View> ); } function ProfileStackScreen() { return ( <ProfileStack.Navigator> <ProfileStack.Screen name="ProfileMain" component={ProfileMainScreen} /> <ProfileStack.Screen name="ProfileEdit" component={ProfileEditScreen} /> </ProfileStack.Navigator> ); } // ----- Tab Navigator ----- type TabParamList = { HomeTab: undefined; ProfileTab: undefined; }; const Tab = createBottomTabNavigator<TabParamList>(); function MainTabNavigator() { return ( <Tab.Navigator screenOptions={({ route }) => ({ tabBarIcon: ({ focused, color, size }) => { const iconName = route.name === 'HomeTab' ? 'home' : 'person'; return <Ionicons name={focused ? iconName : `${iconName}-outline`} size={size} color={color} />; }, tabBarActiveTintColor: '#007AFF', headerShown: false, })} > <Tab.Screen name="HomeTab" component={HomeStackScreen} /> <Tab.Screen name="ProfileTab" component={ProfileStackScreen} /> </Tab.Navigator> ); } export default function NestedStacksApp() { return ( <NavigationContainer> <MainTabNavigator /> </NavigationContainer> ); } // Accessing parent navigators: // navigation.getParent() - Returns parent navigator // navigation.getParent('TabNavigator') - Get specific parent by ID // navigation.dispatch(CommonActions.navigate({ name: 'Tab', params: {} })) // Passing params between nested stacks: // Option 1: Use navigation.getParent()?.navigate('Tab', { screen: 'Screen', params: {} }) // Option 2: Use navigation.dispatch with CommonActions // Option 3: Use global state (Redux, Zustand) for shared data
- Deep Linking with Stack Navigator
Configure deep linking to open specific screens from URLs. React Navigation 8 supports Zod schemas for parameter validation.
// DeepLinkingConfig.tsx import * as React from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { View, Text, Linking } from 'react-native'; import { z } from 'zod'; // React Navigation 8 supports Zod const Stack = createNativeStackNavigator(); // Deep linking configuration const linking = { prefixes: ['myapp://', 'https://myapp.com', 'https://www.myapp.com'], // React Navigation 8 - Automatic path generation // Paths are automatically generated from screen names (PascalCase -> kebab-case) // Example: ProductDetailScreen -> /product-detail-screen // Custom path configuration config: { screens: { Home: { path: '', // root exact: true, }, ProductList: 'products', ProductDetail: { path: 'product/:id', parse: { // React Navigation 8 - Zod schema for validation id: z.coerce.number(), // Convert string to number, reject if not a number }, stringify: { id: (id: number) => id.toString(), }, }, Profile: 'user/:userId', Settings: 'settings', // Nested navigators Modal: { path: 'modal', screens: { ModalContent: 'content', }, }, }, }, // Optional: Get initial URL from the app async getInitialURL() { const url = await Linking.getInitialURL(); return url; }, // Optional: Subscribe to URL events subscribe(listener: (url: string) => void) { const onReceiveURL = ({ url }: { url: string }) => listener(url); Linking.addEventListener('url', onReceiveURL); return () => Linking.removeEventListener('url', onReceiveURL); }, }; function HomeScreen() { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>Home Screen</Text> </View> ); } function ProductListScreen() { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>Product List</Text> </View> ); } function ProductDetailScreen({ route }: { route: any }) { const { id } = route.params; return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>Product Detail - ID: {id}</Text> </View> ); } function ProfileScreen({ route }: { route: any }) { const { userId } = route.params; return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>Profile - User: {userId}</Text> </View> ); } function SettingsScreen() { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>Settings</Text> </View> ); } // React Navigation 8 - Standard Schema support // You can use Zod, Valibot, or ArkType for param validation // Benefits: // - Validation: If validation fails, URL won't match the screen // - Fallback: Schemas called with undefined when param missing // - Better TypeScript inference for required/optional params // For React Navigation 7, use parse function: // parse: { // id: (id: string) => Number(id), // } export default function DeepLinkingApp() { return ( <NavigationContainer linking={linking}> <Stack.Navigator> <Stack.Screen name="Home" component={HomeScreen} /> <Stack.Screen name="ProductList" component={ProductListScreen} /> <Stack.Screen name="ProductDetail" component={ProductDetailScreen} /> <Stack.Screen name="Profile" component={ProfileScreen} /> <Stack.Screen name="Settings" component={SettingsScreen} /> </Stack.Navigator> </NavigationContainer> ); } // URL Examples: // myapp:// // myapp://products // myapp://product/42 // myapp://user/johndoe // https://myapp.com/product/100 // https://www.myapp.com/settings // Testing deep links (iOS Simulator): // xcrun simctl openurl booted myapp://product/123 // Testing deep links (Android Emulator): // adb shell am start -W -a android.intent.action.VIEW -d "myapp://product/123" com.myapp
- TypeScript Best Practices
Full TypeScript integration ensures type safety for navigation and route parameters.
// types/navigation.ts - Global navigation types export type RootStackParamList = { Home: undefined; Details: { id: number; title: string }; Profile: { userId: string; readonly?: boolean }; Modal: undefined; }; // Global type declaration declare global { namespace ReactNavigation { interface RootParamList extends RootStackParamList {} } } // Screen component with full typing import type { NativeStackScreenProps } from '@react-navigation/native-stack'; type DetailsScreenProps = NativeStackScreenProps<RootStackParamList, 'Details'>; function DetailsScreen({ route, navigation }: DetailsScreenProps) { const { id, title } = route.params; // TypeScript knows id is number, title is string return ( <View> <Text>{title}</Text> <Button title="Go Back" onPress={() => navigation.goBack()} /> </View> ); } // useNavigation with type parameter type HomeScreenNavigationProp = NativeStackNavigationProp<RootStackParamList, 'Home'>; function HomeScreen() { const navigation = useNavigation<HomeScreenNavigationProp>(); // Type-safe navigate call navigation.navigate('Details', { id: 42, title: 'Hello' }); // Error: navigation.navigate('Details', { wrong: 'param' }); return null; } // React Navigation 8 - Automatic type inference with createXScreen import { createNativeStackScreen } from '@react-navigation/native-stack'; const Stack = createStackNavigator({ screens: { Profile: createNativeStackScreen({ screen: ProfileScreen, linking: { path: 'user/:userId', parse: { userId: (userId) => Number(userId), }, }, options: ({ route }) => { // route.params.userId is automatically typed as number return { title: `User ${route.params.userId}` }; }, }), }, }); // useRoute with type parameter const route = useRoute<RouteProp<RootStackParamList, 'Details'>>(); const { id, title } = route.params; // Optional params with default values type UserProfileParams = { userId: string; readonly?: boolean; }; function UserProfileScreen({ route }: { route: RouteProp<RootStackParamList, 'Profile'> }) { const { userId, readonly = false } = route.params; return <Text>{userId} - Readonly: {readonly}</Text>; } // Navigation service with TypeScript import { createNavigationContainerRef } from '@react-navigation/native'; export const navigationRef = createNavigationContainerRef<RootStackParamList>(); export function navigate(name: keyof RootStackParamList, params?: any) { if (navigationRef.isReady()) { navigationRef.navigate(name, params); } } // Usage in non-component // import { navigate } from './NavigationService'; // navigate('Details', { id: 1, title: 'Test' });
- Performance Optimization
Optimize stack navigator performance for large apps with lazy loading, screen freezing, and proper memoization.
// PerformanceOptimizations.tsx import * as React from 'react'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { View, Text, ActivityIndicator } from 'react-native'; const Stack = createNativeStackNavigator(); // 1. Lazy loading screens with React.lazy const LazyProfileScreen = React.lazy(() => import('./ProfileScreen')); function LazyLoadedScreen() { return ( <React.Suspense fallback={<ActivityIndicator size="large" />}> <LazyProfileScreen /> </React.Suspense> ); } // 2. Memoize screen components const MemoizedHomeScreen = React.memo(function HomeScreen() { return ( <View> <Text>Memoized Home Screen</Text> </View> ); }); // 3. Freeze inactive screens (React Navigation 8) // Prevents rendering of inactive screens, improves performance function FreezingExample() { return ( <Stack.Navigator screenOptions={{ freezeOnBlur: true, // React Navigation 8 - freeze inactive screens // inactiveBehavior: 'pause', // Clean up effects on inactive screens }} > <Stack.Screen name="Home" component={HomeScreen} /> <Stack.Screen name="Details" component={DetailsScreen} /> </Stack.Navigator> ); } // 4. Detach inactive screens (Native Stack only) // Completely detaches screens from view hierarchy when inactive function DetachInactiveExample() { return ( <Stack.Navigator screenOptions={{ detachInactiveScreens: true, // Native Stack only, improves memory }} > <Stack.Screen name="Home" component={HomeScreen} /> <Stack.Screen name="Details" component={DetailsScreen} /> </Stack.Navigator> ); } // 5. Avoid heavy components in header // Bad: // options={{ // headerRight: () => <ExpensiveComponent /> // Re-renders frequently // }} // Good: // const MemoizedHeaderRight = React.memo(() => <SimpleButton />); // options={{ headerRight: () => <MemoizedHeaderRight /> }} // 6. Use useCallback for navigation handlers function HomeScreenWithHandlers() { const navigation = useNavigation(); const handlePress = React.useCallback(() => { navigation.navigate('Details'); }, [navigation]); return <Button title="Go" onPress={handlePress} />; } // 7. Avoid inline functions in options // Bad: // <Stack.Screen // options={{ // headerRight: () => <Button onPress={() => console.log('press')} /> // New function each render // }} // /> // Good: // const HeaderRight = React.memo(() => <Button onPress={handlePress} />); // <Stack.Screen options={{ headerRight: HeaderRight }} /> // 8. Use getFocusedRouteNameFromRoute to conditionally render tab bars import { getFocusedRouteNameFromRoute } from '@react-navigation/native'; function getTabBarVisibility(route: any) { const routeName = getFocusedRouteNameFromRoute(route) ?? 'Home'; const hideOnScreens = ['Details', 'Profile']; return { display: hideOnScreens.includes(routeName) ? 'none' : 'flex' }; } // Performance Checklist: // ✅ Use freezeOnBlur: true (React Navigation 8) // ✅ Use detachInactiveScreens: true (Native Stack) // ✅ Lazy load heavy screens // ✅ Memoize screen components // ✅ Use useCallback for navigation handlers // ✅ Avoid inline functions in options // ✅ Keep header components simple and memoized // ✅ Profile with React DevTools and Flipper
- Common Patterns & Best Practices
Production-proven patterns for stack navigator implementation.
// NavigationService.ts - Navigation outside components import { createNavigationContainerRef, CommonActions } from '@react-navigation/native'; import { RootStackParamList } from './types'; export const navigationRef = createNavigationContainerRef<RootStackParamList>(); export function navigate(name: keyof RootStackParamList, params?: any) { if (navigationRef.isReady()) { navigationRef.navigate(name, params); } } export function goBack() { if (navigationRef.isReady() && navigationRef.canGoBack()) { navigationRef.goBack(); } } export function resetAndNavigate(name: keyof RootStackParamList) { if (navigationRef.isReady()) { navigationRef.dispatch( CommonActions.reset({ index: 0, routes: [{ name }], }) ); } } // useBeforeRemove.ts - Prevent accidental navigation import { useBeforeRemove } from '@react-navigation/native'; export function usePreventNavigation(hasUnsavedChanges: boolean) { const navigation = useNavigation(); useBeforeRemove((e) => { if (!hasUnsavedChanges) return; e.preventDefault(); Alert.alert( 'Discard changes?', 'You have unsaved changes. Are you sure?', [ { text: 'Stay', style: 'cancel' }, { text: 'Discard', style: 'destructive', onPress: () => navigation.dispatch(e.data.action), }, ] ); }); } // useFocusRefresh.ts - Refresh data on focus import { useFocusEffect } from '@react-navigation/native'; export function useFocusRefresh(refreshFn: () => Promise<void>) { useFocusEffect( React.useCallback(() => { refreshFn(); return () => {}; }, [refreshFn]) ); } // App.tsx - Complete setup with TypeScript import { NavigationContainer } from '@react-navigation/native'; import { navigationRef } from './NavigationService'; export default function App() { return ( <NavigationContainer ref={navigationRef}> <RootNavigator /> </NavigationContainer> ); } // Best Practices Summary: // 1. Use createNativeStackNavigator over createStackNavigator // 2. Define TypeScript param types for all screens // 3. Use useFocusEffect for data fetching on screen focus // 4. Use navigation.push() when you need multiple instances of same screen // 5. Use navigation.replace() for login/onboarding flows // 6. Set freezeOnBlur: true for performance (React Navigation 8) // 7. Use React.memo for screen components // 8. Avoid passing non-serializable data in params // 9. Use getFocusedRouteNameFromRoute to conditionally hide tab bars // 10. Test deep links with simulators and real devices