Lesson 5 of 8

Navigation & Routing

React Navigation, stack, tab, drawer navigators, and deep linking

Navigation in React Native

React Navigation is the most popular navigation library for React Native. It provides native navigation experiences with Stack, Tab, and Drawer navigators.

Installation

# Install React Navigation core
npm install @react-navigation/native

# Install dependencies for Expo
npx expo install react-native-screens react-native-safe-area-context

# Install navigators you need
npm install @react-navigation/native-stack  # Stack navigation
npm install @react-navigation/bottom-tabs   # Tab navigation
npm install @react-navigation/drawer        # Drawer navigation

# For React Native CLI, also run:
cd ios && pod install

Basic Setup

// App.tsx
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import HomeScreen from './screens/HomeScreen';
import DetailsScreen from './screens/DetailsScreen';

// Type-safe navigation
type RootStackParamList = {
  Home: undefined;
  Details: { itemId: number; title: string };
};

const Stack = createNativeStackNavigator<RootStackParamList>();

export default function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator initialRouteName="Home">
        <Stack.Screen 
          name="Home" 
          component={HomeScreen}
          options={{ title: 'My App' }}
        />
        <Stack.Screen 
          name="Details" 
          component={DetailsScreen}
          options={({ route }) => ({ 
            title: route.params.title 
          })}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Stack Navigation

// screens/HomeScreen.tsx
import { View, Text, Button } from 'react-native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';

type Props = NativeStackScreenProps<RootStackParamList, 'Home'>;

function HomeScreen({ navigation }: Props) {
  return (
    <View style={{ flex: 1, padding: 20 }}>
      <Text>Home Screen</Text>
      
      {/* Navigate to Details */}
      <Button
        title="Go to Details"
        onPress={() => navigation.navigate('Details', {
          itemId: 42,
          title: 'Item Details',
        })}
      />
      
      {/* Push adds to stack (can go to same screen again) */}
      <Button
        title="Push Details"
        onPress={() => navigation.push('Details', { itemId: 1 })}
      />
    </View>
  );
}

// screens/DetailsScreen.tsx
function DetailsScreen({ route, navigation }: Props) {
  const { itemId, title } = route.params;
  
  return (
    <View style={{ flex: 1, padding: 20 }}>
      <Text>Details for item: {itemId}</Text>
      
      {/* Go back */}
      <Button title="Go Back" onPress={() => navigation.goBack()} />
      
      {/* Go to first screen in stack */}
      <Button title="Go to Home" onPress={() => navigation.popToTop()} />
      
      {/* Replace current screen */}
      <Button 
        title="Replace" 
        onPress={() => navigation.replace('Details', { itemId: 99 })} 
      />
    </View>
  );
}

Tab Navigation

import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';

type TabParamList = {
  Home: undefined;
  Search: undefined;
  Profile: undefined;
};

const Tab = createBottomTabNavigator<TabParamList>();

function TabNavigator() {
  return (
    <Tab.Navigator
      screenOptions={({ route }) => ({
        tabBarIcon: ({ focused, color, size }) => {
          let iconName: string;
          
          if (route.name === 'Home') {
            iconName = focused ? 'home' : 'home-outline';
          } else if (route.name === 'Search') {
            iconName = focused ? 'search' : 'search-outline';
          } else if (route.name === 'Profile') {
            iconName = focused ? 'person' : 'person-outline';
          }
          
          return <Ionicons name={iconName} size={size} color={color} />;
        },
        tabBarActiveTintColor: '#007AFF',
        tabBarInactiveTintColor: 'gray',
      })}
    >
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen name="Search" component={SearchScreen} />
      <Tab.Screen 
        name="Profile" 
        component={ProfileScreen}
        options={{
          tabBarBadge: 3,  // Show badge
        }}
      />
    </Tab.Navigator>
  );
}

Drawer Navigation

import { createDrawerNavigator } from '@react-navigation/drawer';
import { DrawerContentScrollView, DrawerItem } from '@react-navigation/drawer';

const Drawer = createDrawerNavigator();

function DrawerNavigator() {
  return (
    <Drawer.Navigator
      screenOptions={{
        drawerStyle: {
          backgroundColor: '#fff',
          width: 280,
        },
        drawerActiveTintColor: '#007AFF',
      }}
      drawerContent={(props) => <CustomDrawerContent {...props} />}
    >
      <Drawer.Screen name="Home" component={HomeScreen} />
      <Drawer.Screen name="Settings" component={SettingsScreen} />
    </Drawer.Navigator>
  );
}

// Custom drawer content
function CustomDrawerContent(props) {
  return (
    <DrawerContentScrollView {...props}>
      {/* User profile header */}
      <View style={styles.header}>
        <Image source={{ uri: 'avatar.jpg' }} style={styles.avatar} />
        <Text style={styles.username}>John Doe</Text>
      </View>
      
      {/* Drawer items */}
      <DrawerItem
        label="Home"
        onPress={() => props.navigation.navigate('Home')}
      />
      <DrawerItem
        label="Logout"
        onPress={() => handleLogout()}
      />
    </DrawerContentScrollView>
  );
}

Nested Navigators

// Common pattern: Tabs with Stacks inside
function App() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        {/* Main tabs */}
        <Stack.Screen 
          name="MainTabs" 
          component={TabNavigator}
          options={{ headerShown: false }}
        />
        
        {/* Modal screens on top of tabs */}
        <Stack.Screen 
          name="Modal" 
          component={ModalScreen}
          options={{ presentation: 'modal' }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

function TabNavigator() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="HomeTab" component={HomeStackNavigator} />
      <Tab.Screen name="ProfileTab" component={ProfileStackNavigator} />
    </Tab.Navigator>
  );
}

// Each tab has its own stack
function HomeStackNavigator() {
  return (
    <Stack.Navigator>
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen name="Details" component={DetailsScreen} />
    </Stack.Navigator>
  );
}

💡 Navigation Patterns

  • • Auth Flow: Stack with conditional screens based on login state
  • • Main App: Tab navigator as the main container
  • • Detail Screens: Stack navigator inside each tab
  • • Modals: Stack navigator with presentation: 'modal'

Screen Options & Headers

<Stack.Screen
  name="Details"
  component={DetailsScreen}
  options={{
    // Title
    title: 'Details',
    headerTitle: (props) => <CustomTitle {...props} />,
    
    // Header styling
    headerStyle: { backgroundColor: '#007AFF' },
    headerTintColor: '#fff',
    headerTitleStyle: { fontWeight: 'bold' },
    
    // Header buttons
    headerLeft: () => <BackButton />,
    headerRight: () => <MenuButton />,
    
    // Hide header
    headerShown: false,
    
    // Presentation style
    presentation: 'modal',        // 'card' | 'modal' | 'transparentModal'
    animation: 'slide_from_right', // Animation type
    
    // Gestures
    gestureEnabled: true,
  }}
/>

// Dynamic options from component
function DetailsScreen({ navigation, route }) {
  useLayoutEffect(() => {
    navigation.setOptions({
      title: route.params.title,
      headerRight: () => (
        <Button title="Save" onPress={handleSave} />
      ),
    });
  }, [navigation, route.params.title]);
  
  return <View>...</View>;
}

Deep Linking

// Configure deep links in NavigationContainer
const linking = {
  prefixes: ['myapp://', 'https://myapp.com'],
  config: {
    screens: {
      Home: 'home',
      Details: 'details/:id',
      Profile: {
        path: 'user/:username',
        parse: {
          username: (username) => username.toLowerCase(),
        },
      },
      NotFound: '*',
    },
  },
};

function App() {
  return (
    <NavigationContainer linking={linking} fallback={<LoadingScreen />}>
      <Stack.Navigator>
        {/* screens */}
      </Stack.Navigator>
    </NavigationContainer>
  );
}

// URLs that work:
// myapp://home
// myapp://details/123
// https://myapp.com/user/johndoe

// app.json (Expo) - configure URL schemes
{
  "expo": {
    "scheme": "myapp",
    "ios": {
      "associatedDomains": ["applinks:myapp.com"]
    },
    "android": {
      "intentFilters": [
        {
          "action": "VIEW",
          "data": { "scheme": "https", "host": "myapp.com" }
        }
      ]
    }
  }
}

Auth Flow Pattern

function RootNavigator() {
  const { user, isLoading } = useAuth();
  
  if (isLoading) {
    return <SplashScreen />;
  }
  
  return (
    <Stack.Navigator screenOptions={{ headerShown: false }}>
      {user ? (
        // User is signed in
        <>
          <Stack.Screen name="Main" component={MainTabs} />
          <Stack.Screen name="Settings" component={SettingsScreen} />
        </>
      ) : (
        // User is not signed in
        <>
          <Stack.Screen name="Welcome" component={WelcomeScreen} />
          <Stack.Screen name="Login" component={LoginScreen} />
          <Stack.Screen name="Register" component={RegisterScreen} />
        </>
      )}
    </Stack.Navigator>
  );
}

// Navigation will automatically handle transitions
// when user logs in/out