Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Navigation Fundamentals

Module Overview

Estimated Time: 4 hours | Difficulty: Intermediate | Prerequisites: Core Components, Styling
Navigation is to a mobile app what routing is to a web app — it is the backbone that determines how users move between screens. But mobile navigation is fundamentally different from web routing. On the web, each “page” is a fresh render. In mobile, screens stack on top of each other (like a deck of cards), slide in from the side, or persist behind tabs. Users expect physics-based gestures (swipe back), platform-specific animations, and memory of where they were. React Navigation is the de facto standard for handling all of this in React Native. This module covers 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

A stack navigator works exactly like a stack of physical cards on a desk. Navigating to a new screen places a card on top. Going back removes the top card, revealing the previous one. The native stack navigator (@react-navigation/native-stack) uses the actual native navigation controllers (UINavigationController on iOS, Fragment transactions on Android), which means you get native-quality animations and gestures (swipe-back on iOS) for free.

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 tabs are the most common navigation pattern in mobile apps — Instagram, Twitter, Spotify, and most apps you use daily have them. Tabs let users switch between top-level sections with a single tap. Unlike stack navigation, switching tabs does not push a new screen; it swaps which section is visible while preserving the state of each tab independently.

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

Drawer navigation provides a slide-out menu, commonly used for settings, help, and secondary navigation. In practice, most modern apps have moved away from drawers as the primary navigation pattern (in favor of bottom tabs), but they remain useful for supplementary options. Think of a drawer as the “utility closet” of your app — accessible when needed but not the main path.
// 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

Navigation is where TypeScript pays for itself many times over. Without types, passing the wrong params to a screen is a runtime crash that only surfaces when a user taps a specific button in a specific state. With types, the compiler catches it the moment you write the code. The pattern below centralizes all navigation types in one file and uses CompositeScreenProps to give screens type-safe access to navigators above them in the hierarchy.

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