React Native Navigation – The Complete Guide
React Navigation is the de facto navigation library for React Native apps. Unlike web browsers with built-in history stacks, React Native requires a navigation solution to manage screen transitions, deep linking, and navigation state. React Navigation 8 (currently in alpha) introduces native bottom tabs by default, improved TypeScript inference, pushParams API, and better deep linking support [citation:2][citation:6]. This guide covers both React Navigation 7 (stable) and 8 (next) patterns for production apps.
- Installation & Setup
React Navigation requires several core packages and native dependencies. The installation differs between Expo managed projects and bare React Native projects [citation:1].
# Core navigation packages npm install @react-navigation/native @react-navigation/native-stack # For React Navigation 8 (alpha) npm install @react-navigation/native@next @react-navigation/bottom-tabs@next # Peer dependencies (Expo managed project) npx expo install react-native-screens react-native-safe-area-context # Peer dependencies (bare React Native project) npm install react-native-screens react-native-safe-area-context # iOS pods (bare React Native only) cd ios && pod install && cd .. # Additional navigators (install as needed) npm install @react-navigation/bottom-tabs npm install @react-navigation/drawer npm install react-native-gesture-handler # For Android, add to MainActivity.java: # @Override # protected void onCreate(Bundle savedInstanceState) { # super.onCreate(null); # } # import android.os.Bundle;
- Stack Navigator (Native Stack)
Stack Navigator manages a stack of screens, pushing new screens onto the stack and popping to go back. createNativeStackNavigator uses native UINavigationController on iOS and Fragment on Android for better performance [citation:1][citation:3].
// App.tsx - Basic Stack Navigator import * as React from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { Button, View, Text } from 'react-native'; // Type definitions for navigation type RootStackParamList = { Home: undefined; Details: { itemId: number; title: string }; Profile: { userId: string }; }; const Stack = createNativeStackNavigator<RootStackParamList>(); // Home Screen function HomeScreen({ navigation }: { navigation: any }) { return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 }}> <Text>Home Screen</Text> <Button title="Go to Details" onPress={() => navigation.navigate('Details', { itemId: 42, title: 'The Answer' })} /> <Button title="Push same screen (creates new)" onPress={() => navigation.push('Details', { itemId: 43, title: 'Another One' })} /> <Button title="Go to Profile" onPress={() => navigation.navigate('Profile', { userId: 'user123' })} /> </View> ); } // Details Screen function DetailsScreen({ route, navigation }: { route: any; navigation: any }) { const { itemId, title } = route.params; return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 }}> <Text>Details Screen</Text> <Text>Item ID: {itemId}</Text> <Text>Title: {title}</Text> <Button title="Go Back" onPress={() => navigation.goBack()} /> <Button title="Pop to Top" onPress={() => navigation.popToTop()} /> </View> ); } // Profile Screen function ProfileScreen({ route }: { route: any }) { const { userId } = route.params; return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <Text>Profile Screen for: {userId}</Text> </View> ); } export default function App() { return ( <NavigationContainer> <Stack.Navigator initialRouteName="Home" screenOptions={{ headerStyle: { backgroundColor: '#007AFF' }, headerTintColor: '#fff', headerTitleStyle: { fontWeight: 'bold' }, }} > <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' }} /> </Stack.Navigator> </NavigationContainer> ); } // Navigation methods reference: // navigation.navigate('RouteName', params) - Go 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({ ... }) - Update current screen params
- Bottom Tab Navigator
Bottom Tab Navigator provides a tab bar at the bottom of the screen. In React Navigation 8, native bottom tabs are the default, using react-native-screens for native performance and iOS 26 liquid glass effects [citation:2][citation:6].
// BottomTabNavigator.tsx import * as React from 'react'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { Ionicons } from '@expo/vector-icons'; // or any icon library import { HomeScreen, SettingsScreen, ProfileScreen } from './screens'; // React Navigation 8 - Native bottom tabs (default) // For JS implementation fallback, add: implementation="custom" type TabParamList = { Home: undefined; Settings: undefined; Profile: { userId?: string }; }; const Tab = createBottomTabNavigator<TabParamList>(); export default function BottomTabNavigator() { return ( <Tab.Navigator screenOptions={({ route }) => ({ tabBarIcon: ({ focused, color, size }) => { let iconName: keyof typeof Ionicons.glyphMap = 'home-outline'; if (route.name === 'Home') { iconName = focused ? 'home' : 'home-outline'; } else if (route.name === 'Settings') { iconName = focused ? 'settings' : 'settings-outline'; } else if (route.name === 'Profile') { iconName = focused ? 'person' : 'person-outline'; } return <Ionicons name={iconName} size={size} color={color} />; }, tabBarActiveTintColor: '#007AFF', tabBarInactiveTintColor: 'gray', tabBarStyle: { paddingBottom: 5, height: 60 }, headerStyle: { backgroundColor: '#007AFF' }, headerTintColor: '#fff', })} > <Tab.Screen name="Home" component={HomeScreen} /> <Tab.Screen name="Settings" component={SettingsScreen} /> <Tab.Screen name="Profile" component={ProfileScreen} initialParams={{ userId: 'guest' }} /> </Tab.Navigator> ); } // React Navigation 8 - SF Symbols (iOS) and Material Symbols (Android) // import { SFSymbol, MaterialSymbol } from '@react-navigation/native'; // // function TabBarIcon({ focused, name }: { focused: boolean; name: string }) { // if (Platform.OS === 'ios') { // return <SFSymbol name={name} color={focused ? '#007AFF' : 'gray'} />; // } // return <MaterialSymbol name={name} color={focused ? '#007AFF' : 'gray'} />; // }
- Drawer Navigator
Drawer Navigator provides a side menu that slides from the left (or right) edge. Requires react-native-gesture-handler installation [citation:3][citation:5].
// DrawerNavigator.tsx import * as React from 'react'; import { createDrawerNavigator } from '@react-navigation/drawer'; import { Button, View, Text, StyleSheet } from 'react-native'; import { NavigationContainer } from '@react-navigation/native'; // Must import gesture handler at the top of your entry file import 'react-native-gesture-handler'; type DrawerParamList = { Home: undefined; Notifications: undefined; Settings: undefined; About: undefined; }; const Drawer = createDrawerNavigator<DrawerParamList>(); function HomeScreen({ navigation }: { navigation: any }) { return ( <View style={styles.container}> <Text>Home Screen</Text> <Button title="Open Drawer" onPress={() => navigation.openDrawer()} /> <Button title="Close Drawer" onPress={() => navigation.closeDrawer()} /> <Button title="Toggle Drawer" onPress={() => navigation.toggleDrawer()} /> </View> ); } function NotificationsScreen() { return ( <View style={styles.container}> <Text>Notifications Screen</Text> </View> ); } function SettingsScreen() { return ( <View style={styles.container}> <Text>Settings Screen</Text> </View> ); } // Custom drawer content function CustomDrawerContent({ navigation }: { navigation: any }) { return ( <View style={styles.drawerContent}> <View style={styles.drawerHeader}> <Text style={styles.drawerHeaderText}>My App</Text> </View> <Button title="Go to Home" onPress={() => navigation.navigate('Home')} /> <Button title="Notifications" onPress={() => navigation.navigate('Notifications')} /> <Button title="Settings" onPress={() => navigation.navigate('Settings')} /> <Button title="About" onPress={() => navigation.navigate('About')} /> </View> ); } export default function DrawerNavigator() { return ( <NavigationContainer> <Drawer.Navigator screenOptions={{ drawerStyle: { width: 280, backgroundColor: '#fff' }, drawerActiveTintColor: '#007AFF', drawerInactiveTintColor: 'gray', drawerLabelStyle: { fontSize: 16 }, }} drawerContent={(props) => <CustomDrawerContent {...props} />} > <Drawer.Screen name="Home" component={HomeScreen} /> <Drawer.Screen name="Notifications" component={NotificationsScreen} /> <Drawer.Screen name="Settings" component={SettingsScreen} /> </Drawer.Navigator> </NavigationContainer> ); } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 }, drawerContent: { flex: 1, paddingTop: 40 }, drawerHeader: { padding: 20, borderBottomWidth: 1, borderBottomColor: '#e0e0e0', marginBottom: 20 }, drawerHeaderText: { fontSize: 24, fontWeight: 'bold' }, });
- Nested Navigators
Real apps often combine multiple navigators. Common patterns: Stack inside Tabs, Drawer containing Stack, or Tabs inside Drawer [citation:9].
// NestedNavigators.tsx - Stack inside Tabs inside Drawer import * as React from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { createDrawerNavigator } from '@react-navigation/drawer'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { Ionicons } from '@expo/vector-icons'; // ----- Stack Navigator for Home Tab ----- type HomeStackParamList = { HomeList: undefined; HomeDetail: { id: number }; }; const HomeStack = createNativeStackNavigator<HomeStackParamList>(); function HomeStackScreen() { return ( <HomeStack.Navigator> <HomeStack.Screen name="HomeList" component={HomeListScreen} /> <HomeStack.Screen name="HomeDetail" component={HomeDetailScreen} /> </HomeStack.Navigator> ); } function HomeListScreen({ navigation }: { navigation: any }) { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>Home List</Text> <Button title="Go to Detail" onPress={() => navigation.navigate('HomeDetail', { id: 1 })} /> </View> ); } function HomeDetailScreen({ route }: { route: any }) { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>Home Detail - ID: {route.params.id}</Text> </View> ); } // ----- Stack Navigator for Profile Tab ----- type ProfileStackParamList = { ProfileMain: undefined; ProfileEdit: undefined; }; const ProfileStack = createNativeStackNavigator<ProfileStackParamList>(); function ProfileStackScreen() { return ( <ProfileStack.Navigator> <ProfileStack.Screen name="ProfileMain" component={ProfileMainScreen} /> <ProfileStack.Screen name="ProfileEdit" component={ProfileEditScreen} /> </ProfileStack.Navigator> ); } function ProfileMainScreen({ navigation }: { navigation: any }) { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>Profile Main</Text> <Button title="Edit Profile" onPress={() => navigation.navigate('ProfileEdit')} /> </View> ); } function ProfileEditScreen() { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>Edit Profile</Text> </View> ); } // ----- Bottom 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> ); } // ----- Drawer Navigator (Root) ----- type DrawerParamList = { MainTabs: undefined; Settings: undefined; About: undefined; }; const Drawer = createDrawerNavigator<DrawerParamList>(); function SettingsScreen() { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>Settings</Text> </View> ); } function AboutScreen() { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <Text>About</Text> </View> ); } export default function App() { return ( <NavigationContainer> <Drawer.Navigator screenOptions={{ headerShown: false }}> <Drawer.Screen name="MainTabs" component={MainTabNavigator} options={{ title: 'Home' }} /> <Drawer.Screen name="Settings" component={SettingsScreen} /> <Drawer.Screen name="About" component={AboutScreen} /> </Drawer.Navigator> </NavigationContainer> ); } // Accessing navigation from nested screens: // In HomeDetailScreen, to open drawer: navigation.getParent()?.openDrawer() // To navigate to Settings in drawer: navigation.getParent()?.navigate('Settings')
- Authentication Flow (Conditional Navigation)
Handle authentication screens (Login/Signup) separately from authenticated app screens. React Navigation 8 introduces routeNamesChangeBehavior: 'lastUnhandled' to handle deep links that arrive before authentication completes [citation:6].
// AuthFlow.tsx - Authentication navigation pattern import * as React from 'react'; import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { ActivityIndicator, View } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; // Auth Stack (unauthenticated) const AuthStack = createNativeStackNavigator(); function AuthStackScreen() { return ( <AuthStack.Navigator> <AuthStack.Screen name="Login" component={LoginScreen} /> <AuthStack.Screen name="Signup" component={SignupScreen} /> </AuthStack.Navigator> ); } // App Stack (authenticated) const AppStack = createNativeStackNavigator(); function AppStackScreen() { return ( <AppStack.Navigator> <AppStack.Screen name="Home" component={HomeScreen} /> <AppStack.Screen name="Profile" component={ProfileScreen} /> </AppStack.Navigator> ); } // Root Navigator with conditional rendering const RootStack = createNativeStackNavigator(); export default function App() { const [isLoading, setIsLoading] = React.useState(true); const [userToken, setUserToken] = React.useState<string | null>(null); React.useEffect(() => { // Check if user is logged in const bootstrapAsync = async () => { try { const token = await AsyncStorage.getItem('userToken'); setUserToken(token); } catch (e) { console.error(e); } finally { setIsLoading(false); } }; bootstrapAsync(); }, []); if (isLoading) { return ( <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}> <ActivityIndicator size="large" /> </View> ); } return ( <NavigationContainer> <RootStack.Navigator screenOptions={{ headerShown: false }}> {userToken === null ? ( <RootStack.Screen name="Auth" component={AuthStackScreen} /> ) : ( <RootStack.Screen name="App" component={AppStackScreen} /> )} </RootStack.Navigator> </NavigationContainer> ); } // React Navigation 8 - Handle deep links during auth // <RootStack.Navigator routeNamesChangeBehavior="lastUnhandled"> // This remembers deep links that arrive before auth completes // and processes them once the auth screen is replaced
- Navigation Lifecycle & Hooks
React Navigation provides hooks to respond to screen focus/blur events. In React Navigation 8, inactiveBehavior: 'pause' is the new default, cleaning up effects for inactive screens [citation:2].
// NavigationLifecycle.tsx import * as React from 'react'; import { useFocusEffect, useIsFocused, useNavigation } from '@react-navigation/native'; import { View, Text, Button } from 'react-native'; function ProfileScreen() { // Method 1: useFocusEffect (runs on focus, cleans up on blur) useFocusEffect( React.useCallback(() => { console.log('Screen focused - start fetching data'); // Start timers, subscriptions, fetch data const subscription = someObservable.subscribe(); return () => { console.log('Screen blurred - cleanup'); subscription.unsubscribe(); }; }, []) ); // Method 2: useIsFocused (boolean, triggers re-render) const isFocused = useIsFocused(); return ( <View> <Text>Profile Screen - {isFocused ? 'Focused' : 'Not Focused'}</Text> </View> ); } // React Navigation 8 - Access parent screen navigation function NestedScreen() { // Access navigation object for any parent screen by name const parentNavigation = useNavigation('Home'); const parentRoute = useRoute('Home'); // Get navigation state for parent navigator const focusedRoute = useNavigationState( 'Home', (state) => state.routes[state.index] ); return ( <Button title="Go to Home Root" onPress={() => parentNavigation.navigate('Home')} /> ); } // React Navigation 8 - inactiveBehavior configuration // In navigator options: // screenOptions: { // inactiveBehavior: 'pause' // default - cleans up effects // // 'unmount' - unmount screens completely (Native Stack only) // // 'none' - keep mounted as before // }
- Deep Linking
Deep linking allows opening your app from URLs. React Navigation 8 enables deep linking by default with automatic path generation from screen names (PascalCase to kebab-case) [citation:2][citation:6].
// DeepLinking.tsx import { NavigationContainer } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { z } from 'zod'; // React Navigation 8 supports Zod schemas const Stack = createNativeStackNavigator(); // React Navigation 8 - Deep linking enabled by default // Paths automatically generated: ProfileScreen -> /profile-screen // Custom paths can be configured per screen const linking = { // Custom prefixes (optional in React Navigation 8) prefixes: ['myapp://', 'https://myapp.com'], // Custom path configuration config: { screens: { Home: 'home', Profile: { path: 'user/:userId', parse: { // React Navigation 8 supports Zod schemas userId: z.coerce.number(), // Converts string to number }, }, Settings: 'settings', // Nested navigators Product: { screens: { ProductList: 'products', ProductDetail: 'product/:id', }, }, }, }, // Optional: disable deep linking // enabled: false, }; // React Navigation 8 - Standard Schema support (Zod, Valibot, ArkType) // Benefits over parse functions: // - 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 function App() { return ( <NavigationContainer linking={linking}> <Stack.Navigator> <Stack.Screen name="Home" component={HomeScreen} /> <Stack.Screen name="Profile" component={ProfileScreen} /> <Stack.Screen name="Settings" component={SettingsScreen} /> <Stack.Screen name="ProductList" component={ProductListScreen} /> <Stack.Screen name="ProductDetail" component={ProductDetailScreen} /> </Stack.Navigator> </NavigationContainer> ); } // Handling deep links while app is running import { useDeepLinking } from '@react-navigation/native'; function DeepLinkHandler() { // React Navigation 8 automatically handles deep links // You can listen to deep link events if needed return null; } // URL examples: // myapp://home // myapp://user/123 // https://myapp.com/product/456 // https://myapp.com/products
- TypeScript Integration
React Navigation 8 significantly improves TypeScript inference. Navigation hooks now automatically infer types based on screen names, and path patterns can infer parameter types [citation:2][citation:6].
// types/navigation.ts export type RootStackParamList = { Home: undefined; Details: { id: number; title: string }; Profile: { userId: string }; Modal: { screen: string }; }; // Global type declaration declare global { namespace ReactNavigation { interface RootParamList extends RootStackParamList {} } } // App.tsx with full TypeScript export default function App() { return ( <NavigationContainer> <Stack.Navigator> <Stack.Screen name="Home" component={HomeScreen} /> <Stack.Screen name="Details" component={DetailsScreen} /> <Stack.Screen name="Profile" component={ProfileScreen} /> </Stack.Navigator> </NavigationContainer> ); } // HomeScreen.tsx - typed navigation import { useNavigation, useRoute } from '@react-navigation/native'; import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; import type { RootStackParamList } from '../types/navigation'; type HomeScreenNavigationProp = NativeStackNavigationProp<RootStackParamList, 'Home'>; function HomeScreen() { const navigation = useNavigation<HomeScreenNavigationProp>(); return ( <Button title="Go to Details" onPress={() => navigation.navigate('Details', { id: 42, title: 'Answer' })} /> ); } // React Navigation 8 - Automatic type inference with createXScreen helpers 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}` }; }, }), }, }); // useNavigation with screen name (React Navigation 8) const navigation = useNavigation('Profile'); // Returns typed navigation for Profile screen const route = useRoute('Profile'); // Returns typed route for Profile screen
- Theming & Styling
React Navigation supports custom themes including PlatformColor and DynamicColorIOS. React Navigation 8 introduces Material Design 3 themes for Android [citation:2].
// CustomTheme.tsx import { NavigationContainer, DefaultTheme, DarkTheme } from '@react-navigation/native'; import { useColorScheme, Platform } from 'react-native'; // React Navigation 8 - Material Design 3 themes import { MaterialLightTheme, MaterialDarkTheme } from '@react-navigation/native'; const MyLightTheme = { ...DefaultTheme, colors: { ...DefaultTheme.colors, primary: '#007AFF', background: '#FFFFFF', card: '#F8F9FA', text: '#000000', border: '#E9ECEF', }, }; const MyDarkTheme = { ...DarkTheme, colors: { ...DarkTheme.colors, primary: '#0A84FF', background: '#000000', card: '#1C1C1E', text: '#FFFFFF', border: '#38383A', }, }; // React Navigation 8 - PlatformColor support const PlatformAwareTheme = { ...DefaultTheme, colors: Platform.select({ ios: { ...DefaultTheme.colors, primary: PlatformColor('systemBlue'), background: PlatformColor('systemBackground'), }, android: { ...DefaultTheme.colors, primary: PlatformColor('@android:color/system_primary_light'), background: PlatformColor('@android:color/background'), }, default: DefaultTheme.colors, }), }; export default function App() { const scheme = useColorScheme(); return ( <NavigationContainer theme={scheme === 'dark' ? MyDarkTheme : MyLightTheme}> {/* App content */} </NavigationContainer> ); } // Custom header styling <Stack.Navigator screenOptions={{ headerStyle: { backgroundColor: '#007AFF' }, headerTintColor: '#fff', headerTitleStyle: { fontWeight: 'bold' }, headerBackTitle: 'Back', headerShadowVisible: false, headerLargeTitle: true, // iOS large titles }} > <Stack.Screen name="Home" component={HomeScreen} options={{ title: 'Welcome', headerRight: () => <Button title="Edit" onPress={() => {}} />, }} /> </Stack.Navigator>
- State Persistence
Persist navigation state across app restarts to restore user position. React Navigation 8 introduces a persistor prop that simplifies this [citation:6].
// StatePersistence.tsx import { NavigationContainer } from '@react-navigation/native'; import AsyncStorage from '@react-native-async-storage/async-storage'; // React Navigation 8 - Simplified persistor API function App() { return ( <NavigationContainer persistor={{ async persist(state) { await AsyncStorage.setItem('NAVIGATION_STATE_V1', JSON.stringify(state)); }, async restore() { const state = await AsyncStorage.getItem('NAVIGATION_STATE_V1'); return state ? JSON.parse(state) : undefined; }, }} > <RootNavigator /> </NavigationContainer> ); } // React Navigation 7 - Manual persistence function AppV7() { const [isReady, setIsReady] = React.useState(false); const [initialState, setInitialState] = React.useState(); React.useEffect(() => { const restoreState = async () => { try { const savedState = await AsyncStorage.getItem('NAVIGATION_STATE'); if (savedState) { setInitialState(JSON.parse(savedState)); } } finally { setIsReady(true); } }; restoreState(); }, []); if (!isReady) { return <LoadingScreen />; } return ( <NavigationContainer initialState={initialState} onStateChange={(state) => AsyncStorage.setItem('NAVIGATION_STATE', JSON.stringify(state))} > <RootNavigator /> </NavigationContainer> ); }
- Best Practices & Common Pitfalls
Do's:
- Use
navigation.navigate()for standard navigation,navigation.push()when you need multiple instances of same screen - Use
useFocusEffectinstead ofuseEffectfor data fetching on screen focus - Define navigation param types with TypeScript for type safety
- Use
getFocusedRouteNameFromRouteto dynamically hide tab bars on nested screens - Set
inactiveBehavior: 'pause'(React Navigation 8 default) to clean up effects on inactive screens
Don'ts:
- ❌ Don't nest multiple
NavigationContainercomponents - only one at root - ❌ Don't pass inline functions to
componentprop (causes unmount on re-render) - ❌ Don't rely on
navigation.statedirectly - useuseRoutehook instead - ❌ Don't forget to add
react-native-gesture-handlerimport at top of entry file for Drawer - ❌ Don't ignore Android hardware back button - it works automatically with React Navigation
// Dynamic tab bar hiding based on nested route import { getFocusedRouteNameFromRoute } from '@react-navigation/native'; function HomeTabs() { return ( <Tab.Navigator screenOptions={({ route }) => ({ tabBarStyle: ((route) => { const routeName = getFocusedRouteNameFromRoute(route) ?? 'HomeList'; const hideTabBar = routeName === 'HomeDetail'; return { display: hideTabBar ? 'none' : 'flex' }; })(route), })} > <Tab.Screen name="HomeList" component={HomeListScreen} /> <Tab.Screen name="HomeDetail" component={HomeDetailScreen} /> </Tab.Navigator> ); } // Screen tracking for analytics function App() { const navigationRef = useNavigationContainerRef(); return ( <NavigationContainer ref={navigationRef} onReady={() => { // Navigation is ready }} onStateChange={async (state) => { const currentRouteName = navigationRef.getCurrentRoute()?.name; if (currentRouteName) { await Analytics.trackScreenView(currentRouteName); } }} > <RootNavigator /> </NavigationContainer> ); }