Skip to main content
Navigation Fundamentals

Module Overview

Estimated Time: 4 hours | Difficulty: Intermediate | Prerequisites: Core Components, Styling
Navigation is essential for any mobile app with multiple screens. This module covers React Navigation fundamentals, including stack, tab, and drawer navigators, along with best practices for structuring navigation in enterprise apps. What You’ll Learn:
  • React Navigation setup
  • Stack Navigator
  • Tab Navigator
  • Drawer Navigator
  • Passing parameters
  • Navigation TypeScript types

┌─────────────────────────────────────────────────────────────────────────────┐
│                    React Navigation Architecture                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│   NavigationContainer                                                        │
│   └── Root Navigator (Stack)                                                │
│       ├── Auth Stack                                                        │
│       │   ├── Login Screen                                                  │
│       │   ├── Register Screen                                               │
│       │   └── Forgot Password Screen                                        │
│       │                                                                      │
│       └── Main Stack                                                        │
│           ├── Tab Navigator                                                 │
│           │   ├── Home Tab                                                  │
│           │   │   └── Home Stack                                            │
│           │   │       ├── Home Screen                                       │
│           │   │       └── Details Screen                                    │
│           │   ├── Search Tab                                                │
│           │   ├── Notifications Tab                                         │
│           │   └── Profile Tab                                               │
│           │                                                                  │
│           └── Modal Screens                                                 │
│               ├── Settings Modal                                            │
│               └── Create Post Modal                                         │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Installation & Setup

# Core packages
npm install @react-navigation/native

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

# Navigator packages
npm install @react-navigation/native-stack
npm install @react-navigation/bottom-tabs
npm install @react-navigation/drawer

# For drawer (additional dependency)
npx expo install react-native-gesture-handler react-native-reanimated

Basic Setup

// App.tsx
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { SafeAreaProvider } from 'react-native-safe-area-context';

const Stack = createNativeStackNavigator();

export default function App() {
  return (
    <SafeAreaProvider>
      <NavigationContainer>
        <Stack.Navigator>
          <Stack.Screen name="Home" component={HomeScreen} />
          <Stack.Screen name="Details" component={DetailsScreen} />
        </Stack.Navigator>
      </NavigationContainer>
    </SafeAreaProvider>
  );
}

Stack Navigator

Basic Stack Navigation

// navigation/stacks/HomeStack.tsx
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { HomeScreen } from '@/screens/HomeScreen';
import { DetailsScreen } from '@/screens/DetailsScreen';
import { ProductScreen } from '@/screens/ProductScreen';

export type HomeStackParamList = {
  Home: undefined;
  Details: { id: string; title: string };
  Product: { productId: string };
};

const Stack = createNativeStackNavigator<HomeStackParamList>();

export function HomeStack() {
  return (
    <Stack.Navigator
      initialRouteName="Home"
      screenOptions={{
        headerStyle: {
          backgroundColor: '#3b82f6',
        },
        headerTintColor: '#fff',
        headerTitleStyle: {
          fontWeight: '600',
        },
        headerBackTitleVisible: false,
        animation: 'slide_from_right',
      }}
    >
      <Stack.Screen
        name="Home"
        component={HomeScreen}
        options={{
          title: 'Home',
          headerLargeTitle: true,
        }}
      />
      <Stack.Screen
        name="Details"
        component={DetailsScreen}
        options={({ route }) => ({
          title: route.params.title,
        })}
      />
      <Stack.Screen
        name="Product"
        component={ProductScreen}
        options={{
          presentation: 'modal',
          animation: 'slide_from_bottom',
        }}
      />
    </Stack.Navigator>
  );
}
// screens/HomeScreen.tsx
import { View, Text, Pressable, FlatList, StyleSheet } from 'react-native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { HomeStackParamList } from '@/navigation/stacks/HomeStack';

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

const items = [
  { id: '1', title: 'First Item' },
  { id: '2', title: 'Second Item' },
  { id: '3', title: 'Third Item' },
];

export function HomeScreen({ navigation }: Props) {
  return (
    <View style={styles.container}>
      <FlatList
        data={items}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => (
          <Pressable
            style={styles.item}
            onPress={() => navigation.navigate('Details', {
              id: item.id,
              title: item.title,
            })}
          >
            <Text style={styles.itemText}>{item.title}</Text>
          </Pressable>
        )}
      />
      
      {/* Navigate with push (allows duplicate screens) */}
      <Pressable
        style={styles.button}
        onPress={() => navigation.push('Details', { id: '1', title: 'Pushed' })}
      >
        <Text style={styles.buttonText}>Push Details</Text>
      </Pressable>
    </View>
  );
}

// screens/DetailsScreen.tsx
type DetailsProps = NativeStackScreenProps<HomeStackParamList, 'Details'>;

export function DetailsScreen({ route, navigation }: DetailsProps) {
  const { id, title } = route.params;

  return (
    <View style={styles.container}>
      <Text style={styles.title}>{title}</Text>
      <Text style={styles.subtitle}>ID: {id}</Text>

      {/* Go back */}
      <Pressable style={styles.button} onPress={() => navigation.goBack()}>
        <Text style={styles.buttonText}>Go Back</Text>
      </Pressable>

      {/* Go to specific screen */}
      <Pressable
        style={styles.button}
        onPress={() => navigation.navigate('Home')}
      >
        <Text style={styles.buttonText}>Go to Home</Text>
      </Pressable>

      {/* Pop to top of stack */}
      <Pressable
        style={styles.button}
        onPress={() => navigation.popToTop()}
      >
        <Text style={styles.buttonText}>Pop to Top</Text>
      </Pressable>

      {/* Replace current screen */}
      <Pressable
        style={styles.button}
        onPress={() => navigation.replace('Details', { id: '2', title: 'Replaced' })}
      >
        <Text style={styles.buttonText}>Replace Screen</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
  },
  item: {
    padding: 16,
    backgroundColor: '#f3f4f6',
    borderRadius: 8,
    marginBottom: 8,
  },
  itemText: {
    fontSize: 16,
    color: '#111827',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 8,
  },
  subtitle: {
    fontSize: 16,
    color: '#6b7280',
    marginBottom: 24,
  },
  button: {
    backgroundColor: '#3b82f6',
    padding: 16,
    borderRadius: 8,
    alignItems: 'center',
    marginBottom: 12,
  },
  buttonText: {
    color: '#fff',
    fontWeight: '600',
  },
});

Tab Navigator

Bottom Tab Navigation

// navigation/tabs/MainTabs.tsx
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import { HomeStack } from '../stacks/HomeStack';
import { SearchScreen } from '@/screens/SearchScreen';
import { NotificationsScreen } from '@/screens/NotificationsScreen';
import { ProfileScreen } from '@/screens/ProfileScreen';

export type MainTabParamList = {
  HomeTab: undefined;
  Search: undefined;
  Notifications: undefined;
  Profile: undefined;
};

const Tab = createBottomTabNavigator<MainTabParamList>();

export function MainTabs() {
  return (
    <Tab.Navigator
      screenOptions={({ route }) => ({
        tabBarIcon: ({ focused, color, size }) => {
          let iconName: keyof typeof Ionicons.glyphMap;

          switch (route.name) {
            case 'HomeTab':
              iconName = focused ? 'home' : 'home-outline';
              break;
            case 'Search':
              iconName = focused ? 'search' : 'search-outline';
              break;
            case 'Notifications':
              iconName = focused ? 'notifications' : 'notifications-outline';
              break;
            case 'Profile':
              iconName = focused ? 'person' : 'person-outline';
              break;
            default:
              iconName = 'help-outline';
          }

          return <Ionicons name={iconName} size={size} color={color} />;
        },
        tabBarActiveTintColor: '#3b82f6',
        tabBarInactiveTintColor: '#9ca3af',
        tabBarStyle: {
          backgroundColor: '#fff',
          borderTopWidth: 1,
          borderTopColor: '#e5e7eb',
          paddingBottom: 8,
          paddingTop: 8,
          height: 60,
        },
        tabBarLabelStyle: {
          fontSize: 12,
          fontWeight: '500',
        },
        headerShown: false,
      })}
    >
      <Tab.Screen
        name="HomeTab"
        component={HomeStack}
        options={{
          tabBarLabel: 'Home',
        }}
      />
      <Tab.Screen
        name="Search"
        component={SearchScreen}
        options={{
          headerShown: true,
          headerTitle: 'Search',
        }}
      />
      <Tab.Screen
        name="Notifications"
        component={NotificationsScreen}
        options={{
          headerShown: true,
          headerTitle: 'Notifications',
          tabBarBadge: 3, // Show badge
        }}
      />
      <Tab.Screen
        name="Profile"
        component={ProfileScreen}
        options={{
          headerShown: true,
          headerTitle: 'Profile',
        }}
      />
    </Tab.Navigator>
  );
}

Custom Tab Bar

// components/navigation/CustomTabBar.tsx
import { View, Pressable, StyleSheet } from 'react-native';
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import Animated, {
  useAnimatedStyle,
  withSpring,
  interpolateColor,
} from 'react-native-reanimated';

const AnimatedPressable = Animated.createAnimatedComponent(Pressable);

export function CustomTabBar({ state, descriptors, navigation }: BottomTabBarProps) {
  return (
    <View style={styles.container}>
      {state.routes.map((route, index) => {
        const { options } = descriptors[route.key];
        const isFocused = state.index === index;

        const onPress = () => {
          const event = navigation.emit({
            type: 'tabPress',
            target: route.key,
            canPreventDefault: true,
          });

          if (!isFocused && !event.defaultPrevented) {
            navigation.navigate(route.name);
          }
        };

        return (
          <TabButton
            key={route.key}
            routeName={route.name}
            isFocused={isFocused}
            onPress={onPress}
          />
        );
      })}
    </View>
  );
}

interface TabButtonProps {
  routeName: string;
  isFocused: boolean;
  onPress: () => void;
}

function TabButton({ routeName, isFocused, onPress }: TabButtonProps) {
  const iconMap: Record<string, keyof typeof Ionicons.glyphMap> = {
    HomeTab: 'home',
    Search: 'search',
    Notifications: 'notifications',
    Profile: 'person',
  };

  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: withSpring(isFocused ? 1.1 : 1) }],
    backgroundColor: interpolateColor(
      isFocused ? 1 : 0,
      [0, 1],
      ['transparent', '#eff6ff']
    ),
  }));

  return (
    <AnimatedPressable
      style={[styles.tabButton, animatedStyle]}
      onPress={onPress}
    >
      <Ionicons
        name={isFocused ? iconMap[routeName] : `${iconMap[routeName]}-outline` as any}
        size={24}
        color={isFocused ? '#3b82f6' : '#9ca3af'}
      />
    </AnimatedPressable>
  );
}

const styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    backgroundColor: '#fff',
    paddingVertical: 12,
    paddingHorizontal: 16,
    borderTopWidth: 1,
    borderTopColor: '#e5e7eb',
  },
  tabButton: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    paddingVertical: 8,
    borderRadius: 12,
  },
});

Drawer Navigator

// navigation/drawers/MainDrawer.tsx
import { createDrawerNavigator, DrawerContentScrollView, DrawerItemList, DrawerContentComponentProps } from '@react-navigation/drawer';
import { View, Text, Image, StyleSheet, Pressable } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { MainTabs } from '../tabs/MainTabs';
import { SettingsScreen } from '@/screens/SettingsScreen';
import { HelpScreen } from '@/screens/HelpScreen';
import { useAuth } from '@/hooks/useAuth';

export type DrawerParamList = {
  Main: undefined;
  Settings: undefined;
  Help: undefined;
};

const Drawer = createDrawerNavigator<DrawerParamList>();

export function MainDrawer() {
  return (
    <Drawer.Navigator
      drawerContent={(props) => <CustomDrawerContent {...props} />}
      screenOptions={{
        headerShown: false,
        drawerActiveBackgroundColor: '#eff6ff',
        drawerActiveTintColor: '#3b82f6',
        drawerInactiveTintColor: '#6b7280',
        drawerLabelStyle: {
          fontSize: 16,
          fontWeight: '500',
        },
      }}
    >
      <Drawer.Screen
        name="Main"
        component={MainTabs}
        options={{
          drawerLabel: 'Home',
          drawerIcon: ({ color, size }) => (
            <Ionicons name="home-outline" size={size} color={color} />
          ),
        }}
      />
      <Drawer.Screen
        name="Settings"
        component={SettingsScreen}
        options={{
          headerShown: true,
          drawerIcon: ({ color, size }) => (
            <Ionicons name="settings-outline" size={size} color={color} />
          ),
        }}
      />
      <Drawer.Screen
        name="Help"
        component={HelpScreen}
        options={{
          headerShown: true,
          drawerIcon: ({ color, size }) => (
            <Ionicons name="help-circle-outline" size={size} color={color} />
          ),
        }}
      />
    </Drawer.Navigator>
  );
}

function CustomDrawerContent(props: DrawerContentComponentProps) {
  const { user, logout } = useAuth();

  return (
    <DrawerContentScrollView {...props}>
      {/* User Profile Section */}
      <View style={styles.profileSection}>
        <Image
          source={{ uri: user?.avatar || 'https://via.placeholder.com/80' }}
          style={styles.avatar}
        />
        <Text style={styles.userName}>{user?.name || 'Guest'}</Text>
        <Text style={styles.userEmail}>{user?.email || ''}</Text>
      </View>

      {/* Navigation Items */}
      <DrawerItemList {...props} />

      {/* Logout Button */}
      <View style={styles.footer}>
        <Pressable style={styles.logoutButton} onPress={logout}>
          <Ionicons name="log-out-outline" size={24} color="#ef4444" />
          <Text style={styles.logoutText}>Logout</Text>
        </Pressable>
      </View>
    </DrawerContentScrollView>
  );
}

const styles = StyleSheet.create({
  profileSection: {
    padding: 20,
    borderBottomWidth: 1,
    borderBottomColor: '#e5e7eb',
    marginBottom: 10,
  },
  avatar: {
    width: 80,
    height: 80,
    borderRadius: 40,
    marginBottom: 12,
  },
  userName: {
    fontSize: 18,
    fontWeight: '600',
    color: '#111827',
  },
  userEmail: {
    fontSize: 14,
    color: '#6b7280',
    marginTop: 4,
  },
  footer: {
    padding: 20,
    borderTopWidth: 1,
    borderTopColor: '#e5e7eb',
    marginTop: 'auto',
  },
  logoutButton: {
    flexDirection: 'row',
    alignItems: 'center',
    gap: 12,
  },
  logoutText: {
    fontSize: 16,
    color: '#ef4444',
    fontWeight: '500',
  },
});

TypeScript Navigation Types

Centralized Type Definitions

// navigation/types.ts
import { NavigatorScreenParams, CompositeScreenProps } from '@react-navigation/native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import { DrawerScreenProps } from '@react-navigation/drawer';

// Root Stack
export type RootStackParamList = {
  Auth: NavigatorScreenParams<AuthStackParamList>;
  Main: NavigatorScreenParams<DrawerParamList>;
};

// Auth Stack
export type AuthStackParamList = {
  Login: undefined;
  Register: undefined;
  ForgotPassword: { email?: string };
};

// Drawer
export type DrawerParamList = {
  Tabs: NavigatorScreenParams<MainTabParamList>;
  Settings: undefined;
  Help: undefined;
};

// Main Tabs
export type MainTabParamList = {
  HomeTab: NavigatorScreenParams<HomeStackParamList>;
  Search: undefined;
  Notifications: undefined;
  Profile: undefined;
};

// Home Stack
export type HomeStackParamList = {
  Home: undefined;
  Details: { id: string; title: string };
  Product: { productId: string };
};

// Screen Props Types
export type RootStackScreenProps<T extends keyof RootStackParamList> =
  NativeStackScreenProps<RootStackParamList, T>;

export type AuthStackScreenProps<T extends keyof AuthStackParamList> =
  CompositeScreenProps<
    NativeStackScreenProps<AuthStackParamList, T>,
    RootStackScreenProps<keyof RootStackParamList>
  >;

export type MainTabScreenProps<T extends keyof MainTabParamList> =
  CompositeScreenProps<
    BottomTabScreenProps<MainTabParamList, T>,
    DrawerScreenProps<DrawerParamList>
  >;

export type HomeStackScreenProps<T extends keyof HomeStackParamList> =
  CompositeScreenProps<
    NativeStackScreenProps<HomeStackParamList, T>,
    MainTabScreenProps<keyof MainTabParamList>
  >;

// Declare global types for useNavigation hook
declare global {
  namespace ReactNavigation {
    interface RootParamList extends RootStackParamList {}
  }
}

Using Typed Navigation

// screens/HomeScreen.tsx
import { HomeStackScreenProps } from '@/navigation/types';

type Props = HomeStackScreenProps<'Home'>;

export function HomeScreen({ navigation, route }: Props) {
  // Fully typed navigation
  const goToDetails = () => {
    navigation.navigate('Details', { id: '1', title: 'Item' }); // ✅ Type-safe
    // navigation.navigate('Details', { id: 1 }); // ❌ Error: id must be string
  };

  // Navigate to other stacks
  const goToProfile = () => {
    navigation.navigate('Profile'); // Navigate to tab
  };

  return (
    // ...
  );
}

useNavigation Hook with Types

// hooks/useTypedNavigation.ts
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RootStackParamList } from '@/navigation/types';

export function useTypedNavigation() {
  return useNavigation<NativeStackNavigationProp<RootStackParamList>>();
}

// Usage in any component
function SomeComponent() {
  const navigation = useTypedNavigation();
  
  // Fully typed
  navigation.navigate('Main', { screen: 'Tabs', params: { screen: 'HomeTab' } });
}

Authentication Flow

// navigation/RootNavigator.tsx
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useAuth } from '@/hooks/useAuth';
import { AuthStack } from './stacks/AuthStack';
import { MainDrawer } from './drawers/MainDrawer';
import { LoadingScreen } from '@/screens/LoadingScreen';

const Stack = createNativeStackNavigator<RootStackParamList>();

export function RootNavigator() {
  const { isAuthenticated, isLoading } = useAuth();

  if (isLoading) {
    return <LoadingScreen />;
  }

  return (
    <Stack.Navigator screenOptions={{ headerShown: false }}>
      {isAuthenticated ? (
        <Stack.Screen name="Main" component={MainDrawer} />
      ) : (
        <Stack.Screen name="Auth" component={AuthStack} />
      )}
    </Stack.Navigator>
  );
}

Deep Linking

// navigation/linking.ts
import { LinkingOptions } from '@react-navigation/native';
import * as Linking from 'expo-linking';
import { RootStackParamList } from './types';

const prefix = Linking.createURL('/');

export const linking: LinkingOptions<RootStackParamList> = {
  prefixes: [prefix, 'myapp://', 'https://myapp.com'],
  config: {
    screens: {
      Auth: {
        screens: {
          Login: 'login',
          Register: 'register',
          ForgotPassword: 'forgot-password',
        },
      },
      Main: {
        screens: {
          Tabs: {
            screens: {
              HomeTab: {
                screens: {
                  Home: 'home',
                  Details: 'details/:id',
                  Product: 'product/:productId',
                },
              },
              Search: 'search',
              Notifications: 'notifications',
              Profile: 'profile',
            },
          },
          Settings: 'settings',
        },
      },
    },
  },
};

// App.tsx
<NavigationContainer linking={linking}>
  <RootNavigator />
</NavigationContainer>

Best Practices

Type Everything

Use TypeScript for all navigation params and screen props

Organize by Feature

Group related screens and navigators together

Lazy Load Screens

Use React.lazy for screens not immediately needed

Handle Deep Links

Configure deep linking for better user experience

Next Steps

Module 9: Advanced Navigation

Learn advanced navigation patterns including nested navigators and custom transitions