Skip to main content

Module 6: Navigation with Expo Router

Nested Navigation

Building complex navigation architectures with multiple navigators

🎯 Learning Objectives

  • Understand how nested navigators work together
  • Design navigation architectures for complex apps
  • Navigate between nested navigators correctly
  • Access parent and child navigation states
  • Implement drawer navigation within tab structures
  • Handle common nesting patterns and edge cases
  • Reset navigation state programmatically

Nesting Concepts

Nested navigation occurs when one navigator is rendered inside a screen of another navigator. This creates a hierarchy where each navigator manages its own navigation state independently.

How Nesting Works

In Expo Router, nesting happens automatically based on your file structure. A folder with a _layout.tsx file creates a new navigator level.

app/
β”œβ”€β”€ _layout.tsx              # Level 1: Root Stack
└── (tabs)/
    β”œβ”€β”€ _layout.tsx          # Level 2: Tab Navigator
    └── home/
        β”œβ”€β”€ _layout.tsx      # Level 3: Home Stack
        β”œβ”€β”€ index.tsx
        └── details.tsx
Navigator Nesting Hierarchy Root Stack Navigator app/_layout.tsx Tab Navigator app/(tabs)/_layout.tsx Home Stack home/_layout.tsx index.tsx details.tsx Search Stack search/_layout.tsx index.tsx results.tsx Profile Stack profile/_layout.tsx index.tsx settings.tsx

Key Nesting Principles

πŸ“‹ Nesting Rules

Principle Description
Independent State Each navigator maintains its own navigation state
Parent Controls Child Parent navigator determines if child navigator is visible
Events Bubble Up Navigation events propagate from child to parent
Options Merge Screen options from different levels are merged
Headers Stack Each navigator can show its own header (hide parent's to avoid double headers)

Avoiding Double Headers

A common issue with nesting is double headers. When both parent and child navigators show headers, you see two navigation bars.

// Problem: Both stack and tabs show headers
// app/_layout.tsx
<Stack>
  <Stack.Screen name="(tabs)" />  {/* Shows header */}
</Stack>

// app/(tabs)/_layout.tsx  
<Tabs>
  <Tabs.Screen name="home" />  {/* Also shows header */}
</Tabs>

// Solution: Hide the parent's header for the nested navigator
// app/_layout.tsx
<Stack>
  <Stack.Screen 
    name="(tabs)" 
    options={{ headerShown: false }}  {/* Hide this header */}
  />
</Stack>

βœ… Header Strategy

Generally, show headers at the innermost navigator levelβ€”the one closest to your screens. Hide headers on wrapper navigators that exist purely for structural purposes.

Common Architectures

Let's explore navigation architectures used by popular apps.

Architecture 1: Simple App (Stack with Tabs)

For apps with a main tab interface and occasional full-screen flows:

flowchart TD
    subgraph Root["Root Stack"]
        A["(tabs)"]
        B[Login]
        C[Onboarding]
        D[Settings Modal]
    end
    
    subgraph Tabs["Tab Navigator"]
        E[Home]
        F[Search]
        G[Profile]
    end
    
    A --> Tabs
    
    style A fill:#fff3cd
    style B fill:#fce4ec
    style C fill:#fce4ec
    style D fill:#fce4ec
                
app/
β”œβ”€β”€ _layout.tsx           # Root Stack
β”œβ”€β”€ login.tsx             # Modal
β”œβ”€β”€ onboarding.tsx        # Full screen
β”œβ”€β”€ settings.tsx          # Modal
└── (tabs)/
    β”œβ”€β”€ _layout.tsx       # Tab Navigator
    β”œβ”€β”€ index.tsx         # Home
    β”œβ”€β”€ search.tsx        # Search
    └── profile.tsx       # Profile

Architecture 2: E-commerce App (Tabs with Stacks)

Each tab has its own navigation stack for drilling into content:

app/
β”œβ”€β”€ _layout.tsx                    # Root Stack
β”œβ”€β”€ checkout.tsx                   # Modal (outside tabs)
β”œβ”€β”€ product-quick-view.tsx         # Modal
└── (tabs)/
    β”œβ”€β”€ _layout.tsx                # Tab Navigator
    β”œβ”€β”€ home/
    β”‚   β”œβ”€β”€ _layout.tsx            # Home Stack
    β”‚   β”œβ”€β”€ index.tsx              # Featured products
    β”‚   β”œβ”€β”€ category/[id].tsx      # Category listing
    β”‚   └── product/[id].tsx       # Product detail
    β”œβ”€β”€ search/
    β”‚   β”œβ”€β”€ _layout.tsx            # Search Stack
    β”‚   β”œβ”€β”€ index.tsx              # Search screen
    β”‚   └── results.tsx            # Results
    β”œβ”€β”€ cart/
    β”‚   β”œβ”€β”€ _layout.tsx            # Cart Stack
    β”‚   β”œβ”€β”€ index.tsx              # Cart contents
    β”‚   └── saved.tsx              # Saved for later
    └── account/
        β”œβ”€β”€ _layout.tsx            # Account Stack
        β”œβ”€β”€ index.tsx              # Profile
        β”œβ”€β”€ orders.tsx             # Order history
        β”œβ”€β”€ orders/[id].tsx        # Order detail
        └── settings.tsx           # Settings

Architecture 3: Social App (Complex Nesting)

Social apps often have deeply nested navigation with shared screens:

app/
β”œβ”€β”€ _layout.tsx                    # Root Stack
β”œβ”€β”€ (auth)/                        # Auth group (unauthenticated)
β”‚   β”œβ”€β”€ _layout.tsx                # Auth Stack
β”‚   β”œβ”€β”€ login.tsx
β”‚   β”œβ”€β”€ register.tsx
β”‚   └── forgot-password.tsx
β”œβ”€β”€ (main)/                        # Main group (authenticated)
β”‚   β”œβ”€β”€ _layout.tsx                # Main Stack (wraps tabs)
β”‚   β”œβ”€β”€ create-post.tsx            # Modal
β”‚   β”œβ”€β”€ comments/[id].tsx          # Modal
β”‚   β”œβ”€β”€ user/[id].tsx              # User profile (accessible from anywhere)
β”‚   └── (tabs)/
β”‚       β”œβ”€β”€ _layout.tsx            # Tab Navigator
β”‚       β”œβ”€β”€ feed/
β”‚       β”‚   β”œβ”€β”€ _layout.tsx
β”‚       β”‚   β”œβ”€β”€ index.tsx          # Feed
β”‚       β”‚   └── post/[id].tsx      # Post detail
β”‚       β”œβ”€β”€ explore/
β”‚       β”‚   β”œβ”€β”€ _layout.tsx
β”‚       β”‚   β”œβ”€β”€ index.tsx
β”‚       β”‚   └── hashtag/[tag].tsx
β”‚       β”œβ”€β”€ notifications.tsx       # Single screen
β”‚       └── profile/
β”‚           β”œβ”€β”€ _layout.tsx
β”‚           β”œβ”€β”€ index.tsx          # Own profile
β”‚           β”œβ”€β”€ edit.tsx
β”‚           β”œβ”€β”€ followers.tsx
β”‚           └── following.tsx
└── +not-found.tsx

Implementation Example

// app/_layout.tsx - Root level
import { Stack } from 'expo-router';
import { useAuth } from '@/hooks/useAuth';

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

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

  return (
    <Stack screenOptions={{ headerShown: false }}>
      {!isAuthenticated ? (
        <Stack.Screen name="(auth)" />
      ) : (
        <Stack.Screen name="(main)" />
      )}
    </Stack>
  );
}

// app/(main)/_layout.tsx
import { Stack } from 'expo-router';

export default function MainLayout() {
  return (
    <Stack>
      <Stack.Screen 
        name="(tabs)" 
        options={{ headerShown: false }} 
      />
      <Stack.Screen 
        name="create-post" 
        options={{ 
          presentation: 'modal',
          title: 'Create Post',
        }} 
      />
      <Stack.Screen 
        name="comments/[id]" 
        options={{ 
          presentation: 'modal',
          title: 'Comments',
        }} 
      />
      <Stack.Screen 
        name="user/[id]" 
        options={{ title: 'Profile' }} 
      />
    </Stack>
  );
}

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

export default function TabLayout() {
  return (
    <Tabs screenOptions={{ headerShown: false }}>
      <Tabs.Screen 
        name="feed" 
        options={{
          title: 'Feed',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="home-outline" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen 
        name="explore"
        options={{
          title: 'Explore',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="search-outline" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen 
        name="notifications"
        options={{
          title: 'Notifications',
          headerShown: true, // Single screen, show header
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="notifications-outline" size={size} color={color} />
          ),
        }}
      />
      <Tabs.Screen 
        name="profile"
        options={{
          title: 'Profile',
          tabBarIcon: ({ color, size }) => (
            <Ionicons name="person-outline" size={size} color={color} />
          ),
        }}
      />
    </Tabs>
  );
}

Drawer Navigation

Drawer navigation provides a side menu that slides in from the edge of the screen. It's commonly used for secondary navigation options or settings.

Setting Up Drawer

# Install drawer dependencies
npx expo install @react-navigation/drawer react-native-gesture-handler react-native-reanimated
// app/(drawer)/_layout.tsx
import { Drawer } from 'expo-router/drawer';
import { GestureHandlerRootView } from 'react-native-gesture-handler';

export default function DrawerLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <Drawer>
        <Drawer.Screen
          name="index"
          options={{
            drawerLabel: 'Home',
            title: 'Home',
          }}
        />
        <Drawer.Screen
          name="settings"
          options={{
            drawerLabel: 'Settings',
            title: 'Settings',
          }}
        />
        <Drawer.Screen
          name="about"
          options={{
            drawerLabel: 'About',
            title: 'About',
          }}
        />
      </Drawer>
    </GestureHandlerRootView>
  );
}

Drawer with Tabs

A common pattern is drawer wrapping tabs:

app/
β”œβ”€β”€ _layout.tsx              # Root Stack
└── (drawer)/
    β”œβ”€β”€ _layout.tsx          # Drawer Navigator
    β”œβ”€β”€ (tabs)/
    β”‚   β”œβ”€β”€ _layout.tsx      # Tab Navigator
    β”‚   β”œβ”€β”€ index.tsx        # Home tab
    β”‚   └── search.tsx       # Search tab
    β”œβ”€β”€ settings.tsx         # Drawer item (not in tabs)
    └── about.tsx            # Drawer item (not in tabs)
// app/(drawer)/_layout.tsx
import { Drawer } from 'expo-router/drawer';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { Ionicons } from '@expo/vector-icons';

export default function DrawerLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <Drawer
        screenOptions={{
          drawerActiveBackgroundColor: '#e8f0fe',
          drawerActiveTintColor: '#1a73e8',
          headerShown: false,
        }}
      >
        <Drawer.Screen
          name="(tabs)"
          options={{
            drawerLabel: 'Home',
            drawerIcon: ({ color, size }) => (
              <Ionicons name="home-outline" size={size} color={color} />
            ),
          }}
        />
        <Drawer.Screen
          name="settings"
          options={{
            drawerLabel: 'Settings',
            headerShown: true,
            drawerIcon: ({ color, size }) => (
              <Ionicons name="settings-outline" size={size} color={color} />
            ),
          }}
        />
        <Drawer.Screen
          name="about"
          options={{
            drawerLabel: 'About',
            headerShown: true,
            drawerIcon: ({ color, size }) => (
              <Ionicons name="information-circle-outline" size={size} color={color} />
            ),
          }}
        />
      </Drawer>
    </GestureHandlerRootView>
  );
}

Custom Drawer Content

// components/CustomDrawerContent.tsx
import { View, Text, Image, StyleSheet } from 'react-native';
import {
  DrawerContentScrollView,
  DrawerItemList,
  DrawerItem,
} from '@react-navigation/drawer';
import { useRouter } from 'expo-router';
import { useAuth } from '@/hooks/useAuth';

export function CustomDrawerContent(props: any) {
  const router = useRouter();
  const { user, logout } = useAuth();

  return (
    <DrawerContentScrollView {...props}>
      {/* User Profile Header */}
      <View style={styles.header}>
        <Image
          source={{ uri: user?.avatar }}
          style={styles.avatar}
        />
        <Text style={styles.name}>{user?.name}</Text>
        <Text style={styles.email}>{user?.email}</Text>
      </View>
      
      {/* Default drawer items */}
      <DrawerItemList {...props} />
      
      {/* Custom drawer items */}
      <View style={styles.divider} />
      
      <DrawerItem
        label="Help & Support"
        onPress={() => router.push('/help')}
        icon={({ color, size }) => (
          <Ionicons name="help-circle-outline" size={size} color={color} />
        )}
      />
      
      <DrawerItem
        label="Logout"
        onPress={logout}
        labelStyle={{ color: '#ef4444' }}
        icon={({ size }) => (
          <Ionicons name="log-out-outline" size={size} color="#ef4444" />
        )}
      />
    </DrawerContentScrollView>
  );
}

const styles = StyleSheet.create({
  header: {
    padding: 20,
    borderBottomWidth: 1,
    borderBottomColor: '#e5e7eb',
    marginBottom: 10,
  },
  avatar: {
    width: 60,
    height: 60,
    borderRadius: 30,
    marginBottom: 12,
  },
  name: {
    fontSize: 18,
    fontWeight: 'bold',
  },
  email: {
    fontSize: 14,
    color: '#666',
  },
  divider: {
    height: 1,
    backgroundColor: '#e5e7eb',
    marginVertical: 10,
  },
});

// Use in drawer layout
<Drawer
  drawerContent={(props) => <CustomDrawerContent {...props} />}
>

Opening Drawer Programmatically

import { useNavigation, DrawerActions } from '@react-navigation/native';

function HomeScreen() {
  const navigation = useNavigation();

  const openDrawer = () => {
    navigation.dispatch(DrawerActions.openDrawer());
  };

  const closeDrawer = () => {
    navigation.dispatch(DrawerActions.closeDrawer());
  };

  const toggleDrawer = () => {
    navigation.dispatch(DrawerActions.toggleDrawer());
  };

  return (
    <View>
      <Pressable onPress={openDrawer}>
        <Ionicons name="menu" size={24} />
      </Pressable>
    </View>
  );
}

Cross-Navigator Navigation

Navigating between screens in different navigators requires understanding the navigation hierarchy and using the correct paths.

Absolute vs Relative Paths

import { useRouter } from 'expo-router';

function ProductCard({ productId }: { productId: string }) {
  const router = useRouter();

  // Relative path (within current navigator)
  const goToDetails = () => {
    router.push(`./details/${productId}`);
  };

  // Absolute path (from root)
  const goToCheckout = () => {
    router.push('/checkout');
  };

  // Navigate to different tab
  const goToProfile = () => {
    router.push('/(tabs)/profile');
  };

  // Navigate to nested screen in another tab
  const goToSettings = () => {
    router.push('/(tabs)/profile/settings');
  };

  return (/* ... */);
}

Accessing Parent Navigator

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

function NestedScreen() {
  const navigation = useNavigation();
  
  // Get parent navigator
  const parentNavigation = navigation.getParent();
  
  // Get specific parent by name
  const tabNavigation = navigation.getParent('tabs');
  const rootNavigation = navigation.getParent('root');

  // Navigate using parent
  const goBack = () => {
    // Go back in current navigator
    navigation.goBack();
  };

  const goBackToTab = () => {
    // Go back in parent tab navigator
    parentNavigation?.goBack();
  };

  return (/* ... */);
}

// Name your navigators for easier access
// app/(tabs)/_layout.tsx
<Tabs id="tabs">
  {/* ... */}
</Tabs>

// app/_layout.tsx  
<Stack id="root">
  {/* ... */}
</Stack>

Common Cross-Navigation Scenarios

// Scenario 1: Open modal from any tab
// From: /(tabs)/home/index.tsx
// To: /create-post (modal at root level)
router.push('/create-post');

// Scenario 2: Navigate to screen in different tab
// From: /(tabs)/home/product/[id].tsx
// To: /(tabs)/cart
router.push('/(tabs)/cart');

// Scenario 3: Deep link into a tab's stack
// From: /(tabs)/notifications
// To: /(tabs)/profile/orders/[id]
router.push('/(tabs)/profile/orders/12345');

// Scenario 4: Replace entire navigation (after logout)
router.replace('/(auth)/login');

// Scenario 5: Navigate and reset stack
import { CommonActions } from '@react-navigation/native';

navigation.dispatch(
  CommonActions.reset({
    index: 0,
    routes: [{ name: '(tabs)' }],
  })
);
flowchart TD
    subgraph Current["Current Location"]
        A["/(tabs)/home/product/123"]
    end
    
    B["router.push('./reviews')"] --> C["/(tabs)/home/product/123/reviews"]
    D["router.push('/checkout')"] --> E["/checkout (modal)"]
    F["router.push('/(tabs)/cart')"] --> G["/(tabs)/cart"]
    H["router.replace('/(auth)')"] --> I["/(auth)/login"]
    
    A --> B
    A --> D
    A --> F
    A --> H
    
    style C fill:#e8f5e9
    style E fill:#fce4ec
    style G fill:#fff3cd
    style I fill:#e3f2fd
                

Resetting Navigation State

Sometimes you need to reset navigation stateβ€”after logout, completing a flow, or handling errors. React Navigation provides several methods for this.

Reset to Initial State

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

function LogoutButton() {
  const navigation = useNavigation();

  const handleLogout = async () => {
    await clearUserSession();
    
    // Reset to login screen
    navigation.dispatch(
      CommonActions.reset({
        index: 0,
        routes: [{ name: '(auth)' }],
      })
    );
  };

  return (
    <Pressable onPress={handleLogout}>
      <Text>Logout</Text>
    </Pressable>
  );
}

Reset Specific Navigator

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

function OrderCompleteScreen() {
  const navigation = useNavigation();

  const goToOrders = () => {
    // Pop all screens and push new one
    navigation.dispatch(
      StackActions.popToTop()
    );
    
    // Then navigate to orders
    navigation.navigate('orders');
  };

  // Or reset the entire stack
  const resetToHome = () => {
    navigation.dispatch(
      CommonActions.reset({
        index: 0,
        routes: [
          { name: '(tabs)' },
        ],
      })
    );
  };

  return (/* ... */);
}

Reset Nested Navigator

// Reset a specific nested navigator
const resetHomeStack = () => {
  navigation.dispatch(
    CommonActions.reset({
      index: 0,
      routes: [
        {
          name: '(tabs)',
          state: {
            index: 0,
            routes: [
              {
                name: 'home',
                state: {
                  index: 0,
                  routes: [{ name: 'index' }],
                },
              },
            ],
          },
        },
      ],
    })
  );
};

// Navigate to specific nested state
const navigateToOrderDetail = (orderId: string) => {
  navigation.dispatch(
    CommonActions.reset({
      index: 0,
      routes: [
        {
          name: '(tabs)',
          state: {
            index: 2, // Profile tab
            routes: [
              { name: 'home' },
              { name: 'search' },
              {
                name: 'profile',
                state: {
                  index: 1,
                  routes: [
                    { name: 'index' },
                    { name: 'orders/[id]', params: { id: orderId } },
                  ],
                },
              },
            ],
          },
        },
      ],
    })
  );
};

Using Expo Router's Replace and Dismiss

import { useRouter } from 'expo-router';

function WizardCompleteScreen() {
  const router = useRouter();

  // Replace current screen (no back navigation)
  const finishWizard = () => {
    router.replace('/dashboard');
  };

  // Dismiss modal and navigate
  const dismissAndNavigate = () => {
    router.dismiss(); // Close modal
    router.push('/success');
  };

  // Go back multiple screens
  const goBackMultiple = () => {
    router.dismiss(3); // Go back 3 screens
  };

  return (/* ... */);
}

After Authentication Flow

// Complete auth flow pattern
function LoginScreen() {
  const router = useRouter();

  const handleLogin = async (credentials: Credentials) => {
    try {
      await login(credentials);
      
      // Replace auth stack with main app
      // This prevents going back to login
      router.replace('/(main)/(tabs)');
    } catch (error) {
      Alert.alert('Login Failed', error.message);
    }
  };

  return (/* ... */);
}

function OnboardingComplete() {
  const router = useRouter();

  const finishOnboarding = async () => {
    await markOnboardingComplete();
    
    // Reset entire navigation to start fresh
    router.replace('/');
  };

  return (/* ... */);
}
sequenceDiagram
    participant User
    participant LoginScreen
    participant Auth
    participant Router
    participant MainApp
    
    User->>LoginScreen: Enter credentials
    LoginScreen->>Auth: Authenticate
    Auth-->>LoginScreen: Success
    LoginScreen->>Router: router.replace('/(main)')
    Note over Router: Clears auth stack
    Router->>MainApp: Mount main app
    Note over User,MainApp: Back button won't go to login
                

Hands-On Exercises

Exercise 1: Build a Three-Level Navigation

Create an app with:

  • Root Stack containing auth screens and main app
  • Tab Navigator with 3 tabs: Feed, Messages, Profile
  • Each tab has its own Stack for drilling into content
  • Proper header management (no double headers)
βœ… Solution Structure
app/
β”œβ”€β”€ _layout.tsx              # Root Stack
β”œβ”€β”€ (auth)/
β”‚   β”œβ”€β”€ _layout.tsx          # Auth Stack
β”‚   β”œβ”€β”€ login.tsx
β”‚   └── register.tsx
└── (main)/
    β”œβ”€β”€ _layout.tsx          # Main Stack (headerShown: false)
    └── (tabs)/
        β”œβ”€β”€ _layout.tsx      # Tabs (headerShown: false per tab w/ stack)
        β”œβ”€β”€ feed/
        β”‚   β”œβ”€β”€ _layout.tsx  # Feed Stack
        β”‚   β”œβ”€β”€ index.tsx
        β”‚   └── post/[id].tsx
        β”œβ”€β”€ messages/
        β”‚   β”œβ”€β”€ _layout.tsx  # Messages Stack
        β”‚   β”œβ”€β”€ index.tsx
        β”‚   └── chat/[id].tsx
        └── profile/
            β”œβ”€β”€ _layout.tsx  # Profile Stack
            β”œβ”€β”€ index.tsx
            └── settings.tsx
// app/_layout.tsx
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Screen name="(auth)" />
      <Stack.Screen name="(main)" />
    </Stack>
  );
}

// app/(main)/_layout.tsx
export default function MainLayout() {
  return (
    <Stack screenOptions={{ headerShown: false }}>
      <Stack.Screen name="(tabs)" />
    </Stack>
  );
}

// app/(main)/(tabs)/_layout.tsx
export default function TabLayout() {
  return (
    <Tabs screenOptions={{ headerShown: false }}>
      <Tabs.Screen name="feed" options={{ title: 'Feed' }} />
      <Tabs.Screen name="messages" options={{ title: 'Messages' }} />
      <Tabs.Screen name="profile" options={{ title: 'Profile' }} />
    </Tabs>
  );
}

// app/(main)/(tabs)/feed/_layout.tsx
export default function FeedLayout() {
  return (
    <Stack>
      <Stack.Screen name="index" options={{ title: 'Feed' }} />
      <Stack.Screen name="post/[id]" options={{ title: 'Post' }} />
    </Stack>
  );
}

Exercise 2: Cross-Navigator Navigation

Implement the following navigation scenarios:

  • From Feed tab, navigate to a user's profile (in Profile tab)
  • From any screen, open a "Create Post" modal
  • After creating a post, dismiss modal and navigate to the new post
βœ… Solution
// In Feed screen - navigate to user profile
function PostCard({ post }) {
  const router = useRouter();

  const goToUserProfile = () => {
    // Navigate to profile tab with user ID
    router.push(`/(main)/(tabs)/profile?userId=${post.authorId}`);
  };

  return (
    <Pressable onPress={goToUserProfile}>
      <Text>{post.authorName}</Text>
    </Pressable>
  );
}

// Create Post button (accessible from anywhere)
function CreatePostButton() {
  const router = useRouter();

  return (
    <Pressable onPress={() => router.push('/create-post')}>
      <Ionicons name="add" size={24} />
    </Pressable>
  );
}

// Create Post modal - after success
function CreatePostScreen() {
  const router = useRouter();

  const handleSubmit = async (content: string) => {
    const newPost = await createPost(content);
    
    // Dismiss modal first
    router.dismiss();
    
    // Then navigate to the new post
    setTimeout(() => {
      router.push(`/(main)/(tabs)/feed/post/${newPost.id}`);
    }, 100);
  };

  return (/* ... */);
}

Exercise 3: Drawer with Tabs

Create a drawer navigation that:

  • Contains a tab navigator as its main content
  • Has additional drawer items for Settings and About
  • Includes a custom drawer header with user info
  • Has a logout button that resets navigation
πŸ’‘ Hint

Structure: app/(drawer)/_layout.tsx wraps (tabs)/. Use drawerContent prop for custom content. Import DrawerContentScrollView and DrawerItemList from @react-navigation/drawer.

Exercise 4: Navigation State Reset

Implement a complete logout flow that:

  • Clears user session data
  • Resets all navigation state
  • Navigates to login screen
  • Prevents back navigation to authenticated screens
βœ… Solution
// hooks/useLogout.ts
import { useRouter } from 'expo-router';
import { useAuth } from '@/context/AuthContext';
import AsyncStorage from '@react-native-async-storage/async-storage';

export function useLogout() {
  const router = useRouter();
  const { setUser } = useAuth();

  const logout = async () => {
    try {
      // Clear all stored data
      await AsyncStorage.multiRemove([
        'userToken',
        'userData',
        'NAVIGATION_STATE',
      ]);
      
      // Clear auth context
      setUser(null);
      
      // Replace entire navigation with auth
      router.replace('/(auth)/login');
    } catch (error) {
      console.error('Logout error:', error);
    }
  };

  return { logout };
}

// Usage in settings screen
function SettingsScreen() {
  const { logout } = useLogout();

  const handleLogout = () => {
    Alert.alert(
      'Logout',
      'Are you sure you want to logout?',
      [
        { text: 'Cancel', style: 'cancel' },
        { text: 'Logout', style: 'destructive', onPress: logout },
      ]
    );
  };

  return (
    <Pressable onPress={handleLogout}>
      <Text>Logout</Text>
    </Pressable>
  );
}

Summary

You've mastered nested navigationβ€”the foundation for building complex, production-ready app architectures. You can now design and implement sophisticated navigation patterns.

🎯 Key Takeaways

  • Nesting Hierarchy: Each _layout.tsx creates a navigator level; state is independent per navigator
  • Header Management: Hide parent headers when child navigators provide their own
  • Common Architectures: Root Stack β†’ Tabs β†’ Stacks is the most common pattern
  • Drawer Navigation: Wraps tabs for secondary navigation; requires gesture handler setup
  • Cross-Navigation: Use absolute paths (/checkout) for navigating across navigators
  • State Management: Use useNavigationState to read state, CommonActions to reset
  • Auth Flows: Use router.replace() after login/logout to prevent back navigation

πŸ—οΈ Architecture Decision Guide

Simple app (3-5 screens):
  β†’ Single Stack or Tabs

Standard app (tabs with detail views):
  β†’ Root Stack β†’ Tabs β†’ Stacks per tab

Complex app (auth, multiple sections):
  β†’ Root Stack β†’ Auth Group / Main Group
    β†’ Main: Tabs β†’ Stacks per tab
    β†’ Modals at root level

Enterprise app (drawer + tabs + deep nesting):
  β†’ Root Stack β†’ Drawer β†’ Tabs β†’ Stacks
    β†’ Shared screens at drawer level

What's Next?

In the next lesson, we'll implement Authentication Flowsβ€”protected routes, conditional navigation, and secure patterns for handling user authentication in your app.