reactnative
/

React Native Navigation – Stack, Tab, Drawer & Deep Links

Last Sync: Today

On this page

13
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

reactnative

React Native Navigation – Stack, Tab, Drawer & Deep Links

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.

  1. 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].

BASHRead-only
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;

  1. 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].

React TSXRead-only
1
// 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

  1. 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].

React TSXRead-only
1
// 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'} />;
// }

  1. 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].

React TSXRead-only
1
// 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' },
});

  1. Nested Navigators

Real apps often combine multiple navigators. Common patterns: Stack inside Tabs, Drawer containing Stack, or Tabs inside Drawer [citation:9].

React TSXRead-only
1
// 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')

  1. 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].

React TSXRead-only
1
// 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

  1. 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].

React TSXRead-only
1
// 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
// }

  1. 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].

React TSXRead-only
1
// 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

  1. 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].

React TSXRead-only
1
// 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

  1. Theming & Styling

React Navigation supports custom themes including PlatformColor and DynamicColorIOS. React Navigation 8 introduces Material Design 3 themes for Android [citation:2].

React TSXRead-only
1
// 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>

  1. State Persistence

Persist navigation state across app restarts to restore user position. React Navigation 8 introduces a persistor prop that simplifies this [citation:6].

React TSXRead-only
1
// 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>
  );
}

  1. Best Practices & Common Pitfalls

Do's:

  • Use navigation.navigate() for standard navigation, navigation.push() when you need multiple instances of same screen
  • Use useFocusEffect instead of useEffect for data fetching on screen focus
  • Define navigation param types with TypeScript for type safety
  • Use getFocusedRouteNameFromRoute to 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 NavigationContainer components - only one at root
  • ❌ Don't pass inline functions to component prop (causes unmount on re-render)
  • ❌ Don't rely on navigation.state directly - use useRoute hook instead
  • ❌ Don't forget to add react-native-gesture-handler import at top of entry file for Drawer
  • ❌ Don't ignore Android hardware back button - it works automatically with React Navigation
React TSXRead-only
1
// 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>
  );
}

Test Your Knowledge

Q1
of 4

Which navigation method always pushes a new screen onto the stack, even if one already exists?

A
navigate
B
push
C
goBack
D
replace
Q2
of 4

What is the default value of inactiveBehavior in React Navigation 8?

A
none
B
unmount
C
pause
D
keep
Q3
of 4

Which hook should you use to perform cleanup when a screen loses focus?

A
useEffect
B
useLayoutEffect
C
useFocusEffect
D
useIsFocused
Q4
of 4

What must be imported at the top of the entry file for Drawer Navigator to work?

A
react-native-screens
B
react-native-safe-area-context
C
react-native-gesture-handler
D
@react-navigation/drawer

Frequently Asked Questions

What's the difference between navigate and push?

navigate checks if a screen with that name already exists in the stack. If it does, it jumps to it. If not, it pushes a new screen. push always adds a new screen to the stack, even if one already exists. Use navigate for main navigation flows (prevents duplicate screens). Use push when you need multiple instances of the same screen (e.g., product details).

How do I navigate from a non-component (Redux saga, API service)?

Set up a navigation ref and export it. Create a file NavigationService.ts that holds a ref to the navigator and exposes functions like navigate, goBack, etc. This allows navigation from anywhere in your app, including Redux sagas, API interceptors, and utility functions.

What's new in React Navigation 8?

Key features: native bottom tabs by default (iOS 26 liquid glass effects), automatic TypeScript inference with screen-name parameters, pushParams API for history entries without new screens, routeNamesChangeBehavior for deep links during auth, persistor prop for state persistence, and Standard Schema support (Zod) for deep linking validation.

How do I handle deep links before authentication?

In React Navigation 8, add routeNamesChangeBehavior: 'lastUnhandled' to your root navigator. The navigator remembers deep links that arrive while on the auth screen and processes them automatically after authentication completes. For React Navigation 7, you need to manually store the deep link URL and handle it after login.

Previous

react native layout

Next

react native stack nav

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.