← Back to Lessons

Master React Native Navigation with React Navigation

Why React Navigation?
Stack Navigator: The fundamental pattern

React Navigation (developed by Callstack) is the industry standard. It powers thousands of production apps, including Fortune 500 apps. It's the most mature library, with full support for React Native's new architecture, native optimizations, and a declarative API that makes maintaining large projects easier.

In this lesson, we go straight to what matters: how to use React Navigation to build robust and scalable navigation experiences.

Why React Navigation?

In professional mobile applications, navigation isn't just "changing screens". A professional navigation library must offer:

  • Real native gestures: Swipe back on iOS, back button on Android
  • Platform animations: Transitions that respect Material Design and Human Interface Guidelines
  • Persistent state management: Navigation history survives reloads and deep links
  • Strict typing: TypeScript to prevent navigation errors at compile time
  • New architecture compatibility: Full support for Fabric and Turbo Modules

React Navigation fulfills all this and is actively maintained by Callstack, a company dedicated to the React Native ecosystem.

Fundamentals: NavigationContainer

Every navigation tree in React Navigation must be wrapped in a NavigationContainer. This component: maintains navigation state, manages the app's complete history, integrates deep linking, and connects navigation with the native lifecycle.

Basic structure:

1// App.tsx 2import { NavigationContainer } from '@react-navigation/native'; 3import AppNavigator from './navigation/AppNavigator'; 4 5export default function App() { 6 return ( 7 <NavigationContainer> 8 <AppNavigator /> 9 </NavigationContainer> 10 ); 11}

Note: You only need one NavigationContainer in your entire app, typically in the root component.

Stack Navigator: The fundamental pattern

The Stack Navigator (stack) is the most common navigation pattern. Imagine a stack of cards: each new screen is placed on top, and when going back, the top card is removed.

Native Stack vs JavaScript Stack

React Navigation offers two implementations:

  1. @react-navigation/stack: Fully implemented in JavaScript with Reanimated, more customizable, useful only if you need very specific animations.

Golden rule: Always use native-stack unless you have a very specific reason not to.

Basic implementation with TypeScript

1// src/navigation/types.ts 2export type RootStackParamList = { 3 Home: undefined; 4 Profile: { userId: string; userName: string }; 5 Settings: undefined; 6 Details: { itemId: number }; 7}; 8 9// src/navigation/RootNavigator.tsx 10import React from 'react'; 11import { createNativeStackNavigator } from '@react-navigation/native-stack'; 12import type { RootStackParamList } from './types'; 13 14import HomeScreen from '../screens/HomeScreen'; 15import ProfileScreen from '../screens/ProfileScreen'; 16 17const Stack = createNativeStackNavigator<RootStackParamList>(); 18 19export default function RootNavigator() { 20 return ( 21 <Stack.Navigator 22 initialRouteName="Home" 23 screenOptions={{ 24 headerStyle: { backgroundColor: '#6200ee' }, 25 headerTintColor: '#fff', 26 headerTitleStyle: { fontWeight: 'bold' }, 27 headerBackTitle: 'Back', // iOS 28 animation: 'slide_from_right', // Customize transition 29 }} 30 > 31 <Stack.Screen 32 name="Home" 33 component={HomeScreen} 34 options={{ title: 'Home' }} 35 /> 36 <Stack.Screen 37 name="Profile" 38 component={ProfileScreen} 39 options={({ route }) => ({ 40 title: route.params.userName 41 })} 42 /> 43 </Stack.Navigator> 44 ); 45}

Dynamic screen options

You can configure options based on props or state:

1<Stack.Screen 2 name="Profile" 3 component={ProfileScreen} 4 options={({ route, navigation }) => ({ 5 title: route.params.userName, 6 headerRight: () => ( 7 <Button 8 onPress={() => navigation.navigate('Settings')} 9 title="Config" 10 /> 11 ), 12 })} 13/>

Programmatic navigation: the useNavigation hook

To navigate from any component, use the useNavigation hook:

1import { useNavigation } from '@react-navigation/native'; 2import type { NativeStackNavigationProp } from '@react-navigation/native-stack'; 3import type { RootStackParamList } from '../navigation/types'; 4 5type NavigationProp = NativeStackNavigationProp<RootStackParamList>; 6 7function UserCard({ userId, userName }: Props) { 8 const navigation = useNavigation<NavigationProp>(); 9 10 const handlePress = () => { 11 // TypeScript validates parameters 12 navigation.navigate('Profile', { 13 userId, 14 userName 15 }); 16 }; 17 18 return ( 19 <TouchableOpacity onPress={handlePress}> 20 <Text>{userName}</Text> 21 </TouchableOpacity> 22 ); 23}

Important navigation methods

1// Navigate to a screen 2navigation.navigate('Profile', { userId: '123' }); 3 4// Go back (pop) 5navigation.goBack(); 6 7// Return to a specific screen in the stack 8navigation.navigate('Home'); 9 10// Replace current screen (doesn't allow going back) 11navigation.replace('Login'); 12 13// Clear stack and navigate 14navigation.reset({ 15 index: 0, 16 routes: [{ name: 'Home' }], 17}); 18 19// Go to stack beginning 20navigation.popToTop(); 21 22// Check if you can go back 23if (navigation.canGoBack()) { 24 navigation.goBack(); 25}

Receiving parameters: route.params

From the destination screen, access parameters with route.params:

1import type { NativeStackScreenProps } from '@react-navigation/native-stack'; 2import type { RootStackParamList } from '../navigation/types'; 3 4type Props = NativeStackScreenProps<RootStackParamList, 'Profile'>; 5 6function ProfileScreen({ route, navigation }: Props) { 7 const { userId, userName } = route.params; 8 9 // TypeScript guarantees these parameters exist 10 return ( 11 <View> 12 <Text>User: {userName}</Text> 13 <Text>ID: {userId}</Text> 14 <Button 15 title="Edit" 16 onPress={() => navigation.navigate('EditProfile', { userId })} 17 /> 18 </View> 19 ); 20}

Tab Navigator: Tab navigation

The Tab Navigator is perfect for main sections of your app accessible from a bottom bar. Think of apps like Instagram, Twitter, or any app with persistent navigation.

1// src/navigation/types.ts 2export type MainTabParamList = { 3 HomeTab: undefined; 4 SearchTab: undefined; 5 ProfileTab: { userId: string }; 6 SettingsTab: undefined; 7}; 8 9// src/navigation/TabNavigator.tsx 10import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; 11import Icon from 'react-native-vector-icons/Ionicons'; 12 13const Tab = createBottomTabNavigator<MainTabParamList>(); 14 15export default function TabNavigator() { 16 return ( 17 <Tab.Navigator 18 screenOptions={({ route }) => ({ 19 tabBarIcon: ({ focused, color, size }) => { 20 let iconName: string; 21 22 switch (route.name) { 23 case 'HomeTab': 24 iconName = focused ? 'home' : 'home-outline'; 25 break; 26 case 'SearchTab': 27 iconName = focused ? 'search' : 'search-outline'; 28 break; 29 case 'ProfileTab': 30 iconName = focused ? 'person' : 'person-outline'; 31 break; 32 } 33 34 return <Icon name={iconName} size={size} color={color} />; 35 }, 36 tabBarActiveTintColor: '#6200ee', 37 tabBarInactiveTintColor: 'gray', 38 headerShown: false, // Hide header by default 39 })} 40 > 41 <Tab.Screen 42 name="HomeTab" 43 component={HomeScreen} 44 options={{ tabBarLabel: 'Home' }} 45 /> 46 <Tab.Screen 47 name="SearchTab" 48 component={SearchScreen} 49 options={{ tabBarLabel: 'Search' }} 50 /> 51 <Tab.Screen 52 name="ProfileTab" 53 component={ProfileScreen} 54 options={{ tabBarLabel: 'Profile' }} 55 /> 56 </Tab.Navigator> 57 ); 58}

Hide tabs on specific screens

Sometimes you want to hide the tab bar on certain screens:

1// From the screen 2useLayoutEffect(() => { 3 navigation.getParent()?.setOptions({ 4 tabBarStyle: { display: 'none' } 5 }); 6 7 return () => { 8 navigation.getParent()?.setOptions({ 9 tabBarStyle: undefined 10 }); 11 }; 12}, [navigation]);

Drawer Navigator: Side menu

The Drawer Navigator creates a slidable side menu, common in apps with many sections.

1import { createDrawerNavigator } from '@react-navigation/drawer'; 2 3type DrawerParamList = { 4 Home: undefined; 5 Profile: undefined; 6 Settings: undefined; 7}; 8 9const Drawer = createDrawerNavigator<DrawerParamList>(); 10 11export default function DrawerNavigator() { 12 return ( 13 <Drawer.Navigator 14 screenOptions={{ 15 drawerActiveTintColor: '#6200ee', 16 drawerInactiveTintColor: 'gray', 17 drawerStyle: { 18 backgroundColor: '#f8f8f8', 19 width: 280, 20 }, 21 }} 22 > 23 <Drawer.Screen 24 name="Home" 25 component={HomeScreen} 26 options={{ 27 drawerLabel: 'Home', 28 drawerIcon: ({ color, size }) => ( 29 <Icon name="home" size={size} color={color} /> 30 ), 31 }} 32 /> 33 <Drawer.Screen name="Profile" component={ProfileScreen} /> 34 <Drawer.Screen name="Settings" component={SettingsScreen} /> 35 </Drawer.Navigator> 36 ); 37}

Open/close drawer programmatically

1import { DrawerActions } from '@react-navigation/native'; 2 3function SomeScreen() { 4 const navigation = useNavigation(); 5 6 // Open drawer 7 const openDrawer = () => { 8 navigation.dispatch(DrawerActions.openDrawer()); 9 }; 10 11 // Close drawer 12 const closeDrawer = () => { 13 navigation.dispatch(DrawerActions.closeDrawer()); 14 }; 15 16 // Toggle 17 const toggleDrawer = () => { 18 navigation.dispatch(DrawerActions.toggleDrawer()); 19 }; 20 21 return <Button title="Open menu" onPress={openDrawer} />; 22}

Nested Navigation

In complex apps, it's common to nest navigators. For example: tabs at the top level, each tab with its own stack.

1// Each tab has its own stack 2function HomeStackNavigator() { 3 return ( 4 <Stack.Navigator> 5 <Stack.Screen name="HomeMain" component={HomeScreen} /> 6 <Stack.Screen name="Details" component={DetailsScreen} /> 7 </Stack.Navigator> 8 ); 9} 10 11function ProfileStackNavigator() { 12 return ( 13 <Stack.Navigator> 14 <Stack.Screen name="ProfileMain" component={ProfileScreen} /> 15 <Stack.Screen name="EditProfile" component={EditProfileScreen} /> 16 </Stack.Navigator> 17 ); 18} 19 20// Tab Navigator contains the stacks 21function TabNavigator() { 22 return ( 23 <Tab.Navigator> 24 <Tab.Screen 25 name="HomeTab" 26 component={HomeStackNavigator} 27 options={{ headerShown: false }} 28 /> 29 <Tab.Screen 30 name="ProfileTab" 31 component={ProfileStackNavigator} 32 options={{ headerShown: false }} 33 /> 34 </Tab.Navigator> 35 ); 36}

To navigate from a child stack to a screen in another stack:

1// From HomeScreen (inside HomeTab) 2function HomeScreen() { 3 const navigation = useNavigation(); 4 5 const goToProfile = () => { 6 // Navigate to Profile tab and then to EditProfile 7 navigation.navigate('ProfileTab', { 8 screen: 'EditProfile', 9 params: { userId: '123' }, 10 }); 11 }; 12 13 return <Button title="Go to Edit Profile" onPress={goToProfile} />; 14}

Access parent navigator

1// From a nested screen 2function DetailsScreen() { 3 const navigation = useNavigation(); 4 5 // Access Tab Navigator (parent) 6 const parentNavigation = navigation.getParent(); 7 8 // Switch tab from child stack 9 const switchToProfileTab = () => { 10 parentNavigation?.navigate('ProfileTab'); 11 }; 12 13 return <Button title="Go to Profile tab" onPress={switchToProfileTab} />; 14}

Advanced TypeScript: complete navigation tree typing

For apps with nested navigation, you need to correctly type the entire hierarchy:

1// src/navigation/types.ts 2import type { NavigatorScreenParams } from '@react-navigation/native'; 3 4// Stack inside HomeTab 5export type HomeStackParamList = { 6 HomeMain: undefined; 7 Details: { itemId: number }; 8}; 9 10// Stack inside ProfileTab 11export type ProfileStackParamList = { 12 ProfileMain: undefined; 13 EditProfile: { userId: string }; 14}; 15 16// Main tab 17export type MainTabParamList = { 18 HomeTab: NavigatorScreenParams<HomeStackParamList>; 19 ProfileTab: NavigatorScreenParams<ProfileStackParamList>; 20 SettingsTab: undefined; 21}; 22 23// App root 24export type RootStackParamList = { 25 Auth: undefined; 26 MainApp: NavigatorScreenParams<MainTabParamList>; 27 Modal: { modalType: 'info' | 'warning' }; 28}; 29 30// Helper to type useNavigation in any screen 31declare global { 32 namespace ReactNavigation { 33 interface RootParamList extends RootStackParamList {} 34 } 35}

With this configuration, TypeScript will autocomplete and validate all your navigation:

1// From any screen, regardless of props 2function AnyComponent() { 3 const navigation = useNavigation(); 4 5 // ✅ Full autocompletion 6 navigation.navigate('MainApp', { 7 screen: 'ProfileTab', 8 params: { 9 screen: 'EditProfile', 10 params: { userId: '123' }, 11 }, 12 }); 13}

Deep Linking: Navigation from URLs

Deep linking allows opening your app on a specific screen from an external URL. It's essential for:

  • Push notifications that open specific screens
  • Shared links (e.g., user profile)
  • Marketing campaigns
  • Integration with other apps

Basic deep linking configuration

Native configuration is already ready in your template. You only need to configure React Navigation:

1// App.tsx 2import { NavigationContainer } from '@react-navigation/native'; 3 4const linking = { 5 prefixes: ['myapp://', 'https://myapp.com'], 6 config: { 7 screens: { 8 Home: '', 9 Profile: 'user/:userId', 10 Details: 'item/:itemId', 11 Settings: 'settings', 12 }, 13 }, 14}; 15 16export default function App() { 17 return ( 18 <NavigationContainer linking={linking}> 19 <RootNavigator /> 20 </NavigationContainer> 21 ); 22}

URLs that work with this configuration

  • myapp:// → Home
  • myapp://user/123 → Profile with userId: '123'
  • myapp://item/456 → Details with itemId: '456'
  • https://myapp.com/user/123 → Profile with userId: '123'

Deep linking with nested navigation

For nested navigators (tabs with stacks), structure the config hierarchically:

1const linking = { 2 prefixes: ['myapp://'], 3 config: { 4 screens: { 5 MainApp: { 6 screens: { 7 HomeTab: { 8 screens: { 9 HomeMain: 'home', 10 Details: 'details/:itemId', 11 }, 12 }, 13 ProfileTab: { 14 screens: { 15 ProfileMain: 'profile', 16 EditProfile: 'profile/edit', 17 }, 18 }, 19 }, 20 }, 21 Modal: 'modal/:modalType', 22 }, 23 }, 24};

Resulting URLs:

  • myapp://home → HomeTab > HomeMain
  • myapp://details/123 → HomeTab > Details
  • myapp://profile/edit → ProfileTab > EditProfile

Get the URL that activated the app

1import { Linking } from 'react-native'; 2 3function SomeScreen() { 4 useEffect(() => { 5 // Get initial URL (when app was closed) 6 Linking.getInitialURL().then((url) => { 7 if (url) { 8 console.log('App opened from:', url); 9 } 10 }); 11 12 // Listen for deep links when app is in background 13 const subscription = Linking.addEventListener('url', ({ url }) => { 14 console.log('Deep link received:', url); 15 }); 16 17 return () => subscription.remove(); 18 }, []); 19}

React Navigation supports query params automatically:

1// URL: myapp://profile?userId=123&tab=posts 2function ProfileScreen({ route }) { 3 const { userId, tab } = route.params; 4 // userId: '123', tab: 'posts' 5}

Intercept all state transitions

1import { NavigationContainer } from '@react-navigation/native'; 2 3function App() { 4 const navigationRef = useRef(null); 5 6 return ( 7 <NavigationContainer 8 ref={navigationRef} 9 onStateChange={(state) => { 10 // Analytics: track each screen change 11 const currentRoute = navigationRef.current?.getCurrentRoute(); 12 console.log('Navigated to:', currentRoute?.name); 13 14 // Send to Firebase Analytics, Segment, etc. 15 analytics.logScreenView({ 16 screen_name: currentRoute?.name, 17 screen_class: currentRoute?.name, 18 }); 19 }} 20 onReady={() => { 21 console.log('Navigation ready'); 22 routingInstrumentation.registerNavigationContainer(navigationRef); 23 }} 24 > 25 <RootNavigator /> 26 </NavigationContainer> 27 ); 28}

Get current screen from anywhere

1// Create global reference 2export const navigationRef = createNavigationContainerRef(); 3 4// In App.tsx 5<NavigationContainer ref={navigationRef}> 6 ... 7</NavigationContainer> 8 9// Use from any file (even outside components) 10import { navigationRef } from './navigation/navigationRef'; 11 12export function navigateFromAnywhere(name, params) { 13 if (navigationRef.isReady()) { 14 navigationRef.navigate(name, params); 15 } 16} 17 18// Useful for: 19// - Navigation from push notifications 20// - Navigation from Redux middleware 21// - Navigation from services

Performance optimization

Lazy loading screens

To reduce initial load time, load screens on demand:

1import React, { Suspense } from 'react'; 2 3// Lazy load 4const ProfileScreen = React.lazy(() => import('./screens/ProfileScreen')); 5const SettingsScreen = React.lazy(() => import('./screens/SettingsScreen')); 6 7function RootNavigator() { 8 return ( 9 <Stack.Navigator> 10 <Stack.Screen name="Home" component={HomeScreen} /> 11 12 <Stack.Screen name="Profile"> 13 {(props) => ( 14 <Suspense fallback={<LoadingScreen />}> 15 <ProfileScreen {...props} /> 16 </Suspense> 17 )} 18 </Stack.Screen> 19 20 <Stack.Screen name="Settings"> 21 {(props) => ( 22 <Suspense fallback={<LoadingScreen />}> 23 <SettingsScreen {...props} /> 24 </Suspense> 25 )} 26 </Stack.Screen> 27 </Stack.Navigator> 28 ); 29}

When to use lazy loading? Infrequent screens (advanced settings, legal terms), screens with heavy dependencies, optional feature modules.

Re-render optimization

React Navigation re-renders screens in certain cases. Optimize with React.memo:

1import React, { memo } from 'react'; 2 3const HomeScreen = memo(function HomeScreen({ route, navigation }) { 4 // This component only re-renders if its props change 5 return <View>...</View>; 6}); 7 8export default HomeScreen;

detachInactiveScreens

By default, React Navigation keeps inactive screens mounted. To free memory:

1<Stack.Navigator 2 screenOptions={{ 3 detachInactiveScreens: true, // Unmount inactive screens 4 }} 5> 6 ... 7</Stack.Navigator>

⚠️ Warning: This unmounts the component when leaving, losing local state. Use only if state is in Redux/Context or persisted.