Typescript
The Command Line
react native
mobile development
React Navigation
Callstack
In this lesson, we go straight to what matters: how to use React Navigation to build robust and scalable navigation experiences.
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}
@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.
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}
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/>
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}
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}
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}
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}
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]);
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}
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}
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}
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}
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}
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 allows opening your app on a specific screen from an external URL. It's essential for:
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}
myapp:// → Homemyapp://user/123 → Profile with userId: '123'myapp://item/456 → Details with itemId: '456'https://myapp.com/user/123 → Profile with userId: '123'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 > HomeMainmyapp://details/123 → HomeTab > Detailsmyapp://profile/edit → ProfileTab > EditProfile1import { 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}
1// URL: myapp://profile?userId=123&tab=posts 2function ProfileScreen({ route }) { 3 const { userId, tab } = route.params; 4 // userId: '123', tab: 'posts' 5}
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}
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
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.
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;
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.