Skip to main content
Navigation Patterns

Module Overview

Estimated Time: 3-4 hours | Difficulty: Intermediate | Prerequisites: Core Components
Navigation is one of the most critical aspects of mobile app development. React Navigation is the standard library for routing and navigation in React Native apps. What You’ll Learn:
  • Stack, Tab, and Drawer navigators
  • Expo Router (file-based routing)
  • Passing parameters between screens
  • Deep linking and universal links
  • Custom navigation headers
  • Nested navigation patterns

File-based routing inspired by Next.js:
# Already included in new Expo projects
npx create-expo-app@latest --template tabs

Option 2: React Navigation

Traditional API-based navigation:
# Install React Navigation
npm install @react-navigation/native
npx expo install react-native-screens react-native-safe-area-context

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

Expo Router

Expo Router uses the file system for routing (like Next.js):

File Structure

app/
├── _layout.tsx          # Root layout (navigation container)
├── index.tsx            # Home screen (/)
├── about.tsx            # About screen (/about)
├── (tabs)/              # Tab group
│   ├── _layout.tsx      # Tab bar configuration
│   ├── index.tsx        # First tab
│   ├── explore.tsx      # Second tab
│   └── profile.tsx      # Third tab
├── user/
│   ├── [id].tsx         # Dynamic route (/user/123)
│   └── settings.tsx     # /user/settings
└── +not-found.tsx       # 404 page

Root Layout

// app/_layout.tsx
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';

export default function RootLayout() {
  return (
    <>
      <StatusBar style="auto" />
      <Stack>
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
        <Stack.Screen name="user/[id]" options={{ title: 'User Profile' }} />
        <Stack.Screen name="+not-found" />
      </Stack>
    </>
  );
}

Tab Navigation Layout

// app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';

export default function TabLayout() {
  return (
    <Tabs
      screenOptions={{
        tabBarActiveTintColor: '#3b82f6',
        tabBarInactiveTintColor: '#9ca3af',
        tabBarStyle: {
          backgroundColor: '#fff',
          borderTopWidth: 1,
          borderTopColor: '#e5e7eb',
        },
        headerStyle: {
          backgroundColor: '#fff',
        },
        headerShadowVisible: false,
      }}
    >
      <Tabs.Screen
        name="index"
        options={{
          title: 'Home',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="explore"
        options={{
          title: 'Explore',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="compass" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen
        name="profile"
        options={{
          title: 'Profile',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}
// app/(tabs)/index.tsx
import { View, Text, Pressable, StyleSheet } from 'react-native';
import { Link, useRouter } from 'expo-router';

export default function HomeScreen() {
  const router = useRouter();

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Home Screen</Text>
      
      {/* Method 1: Link component (declarative) */}
      <Link href="/about" asChild>
        <Pressable style={styles.button}>
          <Text style={styles.buttonText}>Go to About</Text>
        </Pressable>
      </Link>
      
      {/* Method 2: useRouter hook (imperative) */}
      <Pressable 
        style={styles.button}
        onPress={() => router.push('/user/123')}
      >
        <Text style={styles.buttonText}>View User 123</Text>
      </Pressable>
      
      {/* With query parameters */}
      <Link 
        href={{
          pathname: '/user/[id]',
          params: { id: '456', tab: 'posts' }
        }} 
        asChild
      >
        <Pressable style={styles.button}>
          <Text style={styles.buttonText}>User with Params</Text>
        </Pressable>
      </Link>
      
      {/* Replace instead of push */}
      <Pressable 
        style={styles.button}
        onPress={() => router.replace('/explore')}
      >
        <Text style={styles.buttonText}>Replace with Explore</Text>
      </Pressable>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
    gap: 16,
  },
  title: {
    fontSize: 28,
    fontWeight: 'bold',
    marginBottom: 20,
  },
  button: {
    backgroundColor: '#3b82f6',
    padding: 16,
    borderRadius: 8,
    alignItems: 'center',
  },
  buttonText: {
    color: '#fff',
    fontSize: 16,
    fontWeight: '600',
  },
});

Dynamic Routes

// app/user/[id].tsx
import { View, Text, StyleSheet } from 'react-native';
import { useLocalSearchParams, useRouter, Stack } from 'expo-router';

export default function UserScreen() {
  const { id, tab } = useLocalSearchParams<{ id: string; tab?: string }>();
  const router = useRouter();

  return (
    <View style={styles.container}>
      {/* Customize header for this screen */}
      <Stack.Screen 
        options={{
          title: `User ${id}`,
          headerRight: () => (
            <Pressable onPress={() => router.push('/user/settings')}>
              <Ionicons name="settings" size={24} color="#3b82f6" />
            </Pressable>
          ),
        }}
      />
      
      <Text style={styles.title}>User Profile</Text>
      <Text style={styles.info}>User ID: {id}</Text>
      {tab && <Text style={styles.info}>Tab: {tab}</Text>}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 20,
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 16,
  },
  info: {
    fontSize: 16,
    color: '#6b7280',
    marginBottom: 8,
  },
});

React Navigation (Traditional)

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';

// Define route params type
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: 'Welcome' }}
        />
        <Stack.Screen 
          name="Details" 
          component={DetailsScreen}
          options={({ route }) => ({ title: route.params.title })}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

Stack Navigator

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

type HomeScreenProps = {
  navigation: NativeStackNavigationProp<RootStackParamList, 'Home'>;
};

export default function HomeScreen({ navigation }: HomeScreenProps) {
  return (
    <View style={{ flex: 1, padding: 20 }}>
      <Text>Home Screen</Text>
      <Button
        title="Go to Details"
        onPress={() => 
          navigation.navigate('Details', { 
            itemId: 42, 
            title: 'Product Details' 
          })
        }
      />
    </View>
  );
}

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

type DetailsScreenProps = {
  navigation: NativeStackNavigationProp<RootStackParamList, 'Details'>;
  route: RouteProp<RootStackParamList, 'Details'>;
};

export default function DetailsScreen({ navigation, route }: DetailsScreenProps) {
  const { itemId, title } = route.params;

  return (
    <View style={{ flex: 1, padding: 20 }}>
      <Text>Item ID: {itemId}</Text>
      <Text>Title: {title}</Text>
      <Button
        title="Go Back"
        onPress={() => navigation.goBack()}
      />
      <Button
        title="Go to Home"
        onPress={() => navigation.popToTop()}
      />
    </View>
  );
}

Tab Navigator

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

const Tab = createBottomTabNavigator();

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

          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';
          } else {
            iconName = 'alert';
          }

          return <Ionicons name={iconName} size={size} color={color} />;
        },
        tabBarActiveTintColor: '#3b82f6',
        tabBarInactiveTintColor: 'gray',
      })}
    >
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen name="Search" component={SearchScreen} />
      <Tab.Screen 
        name="Profile" 
        component={ProfileScreen}
        options={{
          tabBarBadge: 3,  // Show notification badge
        }}
      />
    </Tab.Navigator>
  );
}

Drawer Navigator

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

const Drawer = createDrawerNavigator();

// Custom drawer content
function CustomDrawerContent(props: any) {
  return (
    <DrawerContentScrollView {...props}>
      <View style={styles.drawerHeader}>
        <Image source={{ uri: 'avatar-url' }} style={styles.avatar} />
        <Text style={styles.userName}>John Doe</Text>
        <Text style={styles.userEmail}>[email protected]</Text>
      </View>
      <DrawerItem
        label="Home"
        onPress={() => props.navigation.navigate('Home')}
        icon={({ color, size }) => (
          <Ionicons name="home" size={size} color={color} />
        )}
      />
      <DrawerItem
        label="Settings"
        onPress={() => props.navigation.navigate('Settings')}
        icon={({ color, size }) => (
          <Ionicons name="settings" size={size} color={color} />
        )}
      />
      <DrawerItem
        label="Logout"
        onPress={() => handleLogout()}
        icon={({ color, size }) => (
          <Ionicons name="log-out" size={size} color={color} />
        )}
      />
    </DrawerContentScrollView>
  );
}

function DrawerNavigator() {
  return (
    <Drawer.Navigator
      drawerContent={(props) => <CustomDrawerContent {...props} />}
      screenOptions={{
        drawerActiveBackgroundColor: '#e0e7ff',
        drawerActiveTintColor: '#3b82f6',
      }}
    >
      <Drawer.Screen name="Home" component={HomeScreen} />
      <Drawer.Screen name="Settings" component={SettingsScreen} />
    </Drawer.Navigator>
  );
}

Nested Navigation

Combine multiple navigators for complex navigation patterns:
// Common pattern: Tabs with Stack in each tab

const HomeStack = createNativeStackNavigator();
function HomeStackScreen() {
  return (
    <HomeStack.Navigator>
      <HomeStack.Screen name="HomeMain" component={HomeScreen} />
      <HomeStack.Screen name="Details" component={DetailsScreen} />
    </HomeStack.Navigator>
  );
}

const ProfileStack = createNativeStackNavigator();
function ProfileStackScreen() {
  return (
    <ProfileStack.Navigator>
      <ProfileStack.Screen name="ProfileMain" component={ProfileScreen} />
      <ProfileStack.Screen name="EditProfile" component={EditProfileScreen} />
      <ProfileStack.Screen name="Settings" component={SettingsScreen} />
    </ProfileStack.Navigator>
  );
}

function App() {
  return (
    <NavigationContainer>
      <Tab.Navigator>
        <Tab.Screen 
          name="Home" 
          component={HomeStackScreen}
          options={{ headerShown: false }}
        />
        <Tab.Screen 
          name="Profile" 
          component={ProfileStackScreen}
          options={{ headerShown: false }}
        />
      </Tab.Navigator>
    </NavigationContainer>
  );
}

Deep Linking

Expo Router Deep Linking

// app.json
{
  "expo": {
    "scheme": "myapp",
    "web": {
      "bundler": "metro"
    }
  }
}
Links like myapp://user/123 will automatically route to /user/123.

React Navigation Deep Linking

const linking = {
  prefixes: ['myapp://', 'https://myapp.com'],
  config: {
    screens: {
      Home: 'home',
      Details: 'details/:id',
      Profile: {
        path: 'user/:userId',
        screens: {
          Settings: 'settings',
        },
      },
    },
  },
};

function App() {
  return (
    <NavigationContainer linking={linking}>
      {/* navigators */}
    </NavigationContainer>
  );
}

useNavigationState

import { useNavigationState } from '@react-navigation/native';

function MyComponent() {
  // Get current route name
  const routeName = useNavigationState(
    state => state.routes[state.index].name
  );
  
  // Get previous route
  const prevRoute = useNavigationState(state => {
    const routes = state.routes;
    return routes.length > 1 ? routes[routes.length - 2] : null;
  });

  return <Text>Current: {routeName}</Text>;
}

Focus/Blur Events

import { useFocusEffect } from '@react-navigation/native';
import { useCallback } from 'react';

function ProfileScreen() {
  // Runs when screen comes into focus
  useFocusEffect(
    useCallback(() => {
      console.log('Screen focused - fetch data');
      fetchProfileData();

      return () => {
        console.log('Screen unfocused - cleanup');
      };
    }, [])
  );

  return <View />;
}

Prevent Going Back

import { useNavigation } from '@react-navigation/native';
import { useEffect } from 'react';
import { Alert } from 'react-native';

function EditScreen() {
  const navigation = useNavigation();
  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);

  useEffect(() => {
    const unsubscribe = navigation.addListener('beforeRemove', (e) => {
      if (!hasUnsavedChanges) return;

      e.preventDefault();

      Alert.alert(
        'Discard changes?',
        'You have unsaved changes. Are you sure you want to leave?',
        [
          { text: "Don't leave", style: 'cancel' },
          {
            text: 'Discard',
            style: 'destructive',
            onPress: () => navigation.dispatch(e.data.action),
          },
        ]
      );
    });

    return unsubscribe;
  }, [navigation, hasUnsavedChanges]);

  return <View />;
}

Custom Headers

// Custom header component
function CustomHeader({ title, onBack }: { title: string; onBack: () => void }) {
  return (
    <View style={styles.header}>
      <Pressable onPress={onBack} style={styles.backButton}>
        <Ionicons name="arrow-back" size={24} color="#000" />
      </Pressable>
      <Text style={styles.headerTitle}>{title}</Text>
      <View style={styles.placeholder} />
    </View>
  );
}

// Use in navigator
<Stack.Screen
  name="Details"
  component={DetailsScreen}
  options={{
    header: ({ navigation, route }) => (
      <CustomHeader 
        title={route.params?.title || 'Details'}
        onBack={() => navigation.goBack()}
      />
    ),
  }}
/>

const styles = StyleSheet.create({
  header: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    paddingHorizontal: 16,
    paddingTop: 50,  // Account for status bar
    paddingBottom: 16,
    backgroundColor: '#fff',
    borderBottomWidth: 1,
    borderBottomColor: '#e5e7eb',
  },
  backButton: {
    padding: 8,
  },
  headerTitle: {
    fontSize: 18,
    fontWeight: 'bold',
  },
  placeholder: {
    width: 40,
  },
});

┌──────────────────────────────────────────────────────────────────────┐
│                    Navigation Methods                                 │
├──────────────────────────────────────────────────────────────────────┤
│                                                                       │
│  Stack Navigation:                                                    │
│  ─────────────────                                                    │
│  navigate('Screen')     → Go to screen (won't duplicate)             │
│  push('Screen')         → Always add to stack (can duplicate)        │
│  goBack()               → Go to previous screen                       │
│  popToTop()             → Go to first screen in stack                │
│  replace('Screen')      → Replace current screen                      │
│                                                                       │
│  Tab Navigation:                                                      │
│  ───────────────                                                      │
│  navigate('TabName')    → Switch to tab                              │
│  jumpTo('TabName')      → Same as navigate for tabs                  │
│                                                                       │
│  Drawer Navigation:                                                   │
│  ─────────────────                                                    │
│  openDrawer()           → Open drawer                                │
│  closeDrawer()          → Close drawer                               │
│  toggleDrawer()         → Toggle drawer state                        │
│                                                                       │
│  Common Patterns:                                                     │
│  ───────────────                                                      │
│  setParams({ key: value })  → Update route params                    │
│  setOptions({ title: '' })  → Update screen options                  │
│  reset({ routes: [...] })   → Reset navigation state                 │
│                                                                       │
└──────────────────────────────────────────────────────────────────────┘

Summary

Navigator Types

  • Stack: Push/pop screens
  • Tab: Parallel screens
  • Drawer: Side menu
  • Nested: Combine navigators

Key Concepts

  • Deep linking
  • Screen params
  • Navigation events
  • Custom headers

Next Module

State Management

Learn local and global state management patterns