🔴 LIVE 1-ON-1 MASTERCLASS

Module 1 • Lesson 03 Expo Router & Multi-Screen Navigation

Mastering File-Based Routing, Stack Navigators, Dynamic Route Segments ([id]), useLocalSearchParams, and Native Screen Transitions.

👨‍🏫 Live Teacher Concept Breakdown (Simple Bhasha Me)

Altamash, mobile navigation ko master karne ke liye ye 3 step-by-step mental models samajhna zaroori hai:

1. 📂 File-Based Routing (Files = Routes):

Next.js ki tarah, app/index.tsx automatically Home screen banta hai, app/add-meal.tsx naya /add-meal route banta hai, aur app/meal/[id].tsx dynamic detail screen ban jata hai.

2. 📚 Stack Navigator (Card Deck):

Screens ek ke upar ek stack hoti hain. router.push() se naya screen upar PUSH hota hai, aur router.back() se previous screen par wapas POP ho jate hain.

3. 🔗 Data Passing via useLocalSearchParams:

List se detail screen tak data bhejne ke liye params pass karte hain aur detail screen me useLocalSearchParams() hook se unhe extract kar lete hain.

🎯 Today's Target Milestone

1. Understand how File-Based Routing automatically generates native screens from files.
2. Configure a clean, dark-themed Stack Navigator Layout in app/_layout.tsx.
3. Perform screen navigation using Link and the useRouter() hook.
4. Build dynamic parameter-driven screens using [id].tsx and useLocalSearchParams.
5. Integrate everything into a multi-screen MacroZone Nutritional Tracker.

1. File-Based Routing & Root Stack Layout (The Basics First)

In Expo Router, your folder structure in app/ defines your screens. The file app/_layout.tsx acts as the parent container (Navigator) that wraps all child screens.

📱 Route Mapping Anatomy
1

app/_layout.tsx → Root Stack Navigator

Sets common dark theme background, header colors, and screen transition rules.

↓ wraps
app/index.tsx
Route: / (Home Screen)
app/details.tsx
Route: /details (Detail Screen)
Simple Isolated Example: app/_layout.tsx
import { Stack } from "expo-router"; // 👈 Stack navigator primitive import

export default function SimpleLayout() {
  return (
    // 👈 screenOptions applies dark background and header style to all screens
    <Stack
      screenOptions={{
        headerStyle: { backgroundColor: "#020617" },  // 👈 Dark Slate-950 header background
        headerTintColor: "#818cf8",                   // 👈 Indigo-400 color for title & back button
      }}
    >
      {/* 👈 Screen 1: Home Screen (Header hidden because index has its own header) */}
      <Stack.Screen name="index" options={{ headerShown: false }} />
      
      {/* 👈 Screen 2: Details Screen (Shows native top header with back arrow) */}
      <Stack.Screen name="details" options={{ title: "Item Details" }} />
    </Stack>
  );
}

🔍 Line-by-Line Explanation:

Code Line Kya Karta Hai? (Simple Explanation)
import { Stack } from "expo-router" Native mobile stack navigation component import karta hai.
<Stack screenOptions={{ ... }}> Sabhi screens ke liye default background aur header styles define karta hai.
headerShown: false Native default header ko hide karta hai taaki hum screen me custom UI design kar sakein.
title: "Item Details" Top navigation bar me screen ka title text set karta hai.

2. Navigating Between Screens: useRouter() & <Link>

To move between screens, React Native gives you two ways:

📚 Screen Stack Lifecycle
➡️ router.push('/details') PUSH Action
Screen 1: Home (Base)
Screen 2: Details (Pushed on Top)

Naya screen stack ke upar slide ho jata hai.

⬅️ router.back() POP Action
Screen 1: Home (Revealed)

Top screen remove ho jata hai aur purana screen visible hota hai.

Simple Isolated Example: Navigation Component
import { useRouter } from "expo-router";               // 👈 Navigation hook import
import { View, Text, Pressable } from "react-native";

export default function SimpleNavigationDemo() {
  const router = useRouter(); // 👈 Navigation controller instance

  return (
    // 👈 bg-slate-900 border border-slate-800 p-4: dark surface card
    <View className="bg-slate-900 border border-slate-800 p-4 rounded-2xl gap-3">
      <Text className="text-white font-bold">Screen Navigation Demo</Text>
      
      {/* 👈 1. Push Action Button: naya screen stack me add karta hai */}
      <Pressable
        onPress={() => router.push("/details")}
        className="bg-indigo-600 active:bg-indigo-700 py-3 rounded-xl items-center"
      >
        <Text className="text-white font-semibold">Go to Details Screen ➔</Text>
      </Pressable>

      {/* 👈 2. Back Action Button: purane screen par wapas le jata hai */}
      <Pressable
        onPress={() => router.back()}
        className="bg-slate-800 active:bg-slate-700 py-3 rounded-xl items-center"
      >
        <Text className="text-slate-300 font-semibold">← Go Back</Text>
      </Pressable>
    </View>
  );
}

🔍 Line-by-Line Explanation:

Code Line Kya Karta Hai? (Simple Explanation)
const router = useRouter() Screen navigation trigger karne ke liye router instance create karta hai.
router.push("/details") /details screen ko stack ke upar push (navigate) karta hai.
router.back() Active screen ko dismiss (pop) karke previous screen par return karta hai.

3. Dynamic Routes ([id].tsx) & Passing Parameters (useLocalSearchParams)

Jab kisi list ke item par click karke uski individual details dikhani ho, hum Dynamic Routes use karte hain:

  1. File ka naam brackets me rakhte hain: app/meal/[id].tsx.
  2. Sender screen (List) se data pass karte hain: router.push({ pathname: '/meal/[id]', params: { id: '1', name: 'Oats' } }).
  3. Receiver screen (Detail) me useLocalSearchParams() hook se data receive karte hain.
🔄 Dynamic Parameter Transmission Flow
A

Sender (Home List Screen)

router.push({ pathname: "/meal/[id]", params: { id: "1", name: "Oats", calories: "420" } })

↓ URL params serialized
B

Receiver: app/meal/[id].tsx

const { id, name, calories } = useLocalSearchParams()

Simple Isolated Example: app/meal/[id].tsx
import { useLocalSearchParams, useRouter } from "expo-router"; // 👈 Params hook import
import { View, Text, Pressable } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";

export default function SimpleMealDetail() {
  const router = useRouter(); // 👈 Back navigation ke liye
  
  // 👈 URL parameters ko extract karta hai
  const { id, name, calories } = useLocalSearchParams<{
    id: string;
    name: string;
    calories: string;
  }>();

  return (
    // 👈 flex-1 bg-slate-950 px-5 pt-4: full-screen dark container
    <SafeAreaView className="flex-1 bg-slate-950 px-5 pt-4">
      
      {/* 👈 Back Button */}
      <Pressable
        onPress={() => router.back()}
        className="bg-slate-900 border border-slate-800 px-4 py-2 rounded-xl mb-4 w-28 items-center"
      >
        <Text className="text-indigo-400 font-bold">← Back</Text>
      </Pressable>

      {/* 👈 Dynamic Meal Card */}
      <View className="bg-slate-900 border border-slate-800 p-5 rounded-2xl gap-2">
        <Text className="text-slate-400 text-xs">Item ID: #{id}</Text>
        <Text className="text-white text-2xl font-bold">{name}</Text>
        <Text className="text-indigo-400 font-semibold text-lg">{calories} kcal</Text>
      </View>
    </SafeAreaView>
  );
}

🔍 Line-by-Line Explanation:

Code Line Kya Karta Hai? (Simple Explanation)
const { id, name, calories } = useLocalSearchParams() Passed route parameters (ID, Name, Calories) ko direct string variables me extract karta hai.
<Text>{name}</Text> Dynamic meal name ko detail screen ke hero heading me render karta hai.
router.back() User ko wapas previous home list screen par bhejta hai.

4. 🎨 Tailwind CSS (NativeWind) Utility Decoder for Navigation UI

Here is the visual cheat sheet for all Tailwind utility classes used in our navigation layouts:

📐 Layout & Alignment Classes

🌑 Dark Theme & Feedback Palette

5. Complete Integration: MacroZone Multi-Screen Navigation

Ab jab humne teeno concepts alag-alag simple examples me samajh liye hain, chaliye dekhte hain ki ye sab MacroZone app me ek saath kaise seamlessly connect hote hain!

app/_layout.tsx (Full Root Stack Navigator)
import "../global.css";                                 // 👈 Tailwind stylesheet import
import { Stack } from "expo-router";                    // 👈 Stack navigator

export default function RootLayout() {
  return (
    <Stack
      screenOptions={{
        headerStyle: { backgroundColor: "#020617" },    // 👈 Slate-950 dark header
        headerTintColor: "#818cf8",                     // 👈 Indigo-400 accent
        headerTitleStyle: { fontWeight: "bold" },
        contentStyle: { backgroundColor: "#020617" },
      }}
    >
      {/* 👈 Screen 1: Dashboard */}
      <Stack.Screen name="index" options={{ title: "MacroZone", headerShown: false }} />

      {/* 👈 Screen 2: Dynamic Meal Detail Breakdown */}
      <Stack.Screen name="meal/[id]" options={{ title: "Meal Details", headerBackTitle: "Back" }} />

      {/* 👈 Screen 3: Add Meal Modal Sheet */}
      <Stack.Screen name="add-meal" options={{ title: "Log New Meal", presentation: "modal" }} />
    </Stack>
  );
}
app/index.tsx (MacroZone Dashboard with Clickable Meal Cards)
import { useState } from "react";
import { View, Text, TextInput, Pressable, FlatList } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useRouter, Link } from "expo-router";                 // 👈 Router hooks

export interface MealItem {
  id: string;
  name: string;
  calories: number;
  protein: number;
}

export default function MacroZoneHome() {
  const router = useRouter(); // 👈 Navigation controller instance

  const [meals, setMeals] = useState<MealItem[]>([
    { id: "1", name: "Oats & Whey Protein", calories: 420, protein: 35 },
    { id: "2", name: "Grilled Chicken Salad", calories: 510, protein: 48 },
    { id: "3", name: "Boiled Eggs & Almonds", calories: 280, protein: 18 },
  ]);

  const [name, setName] = useState("");
  const [calories, setCalories] = useState("");
  const [protein, setProtein] = useState("");

  const totalCalories = meals.reduce((sum, m) => sum + m.calories, 0);
  const totalProtein = meals.reduce((sum, m) => sum + m.protein, 0);

  const handleAddMeal = () => {
    if (!name.trim() || !calories.trim()) return;
    const newMeal: MealItem = {
      id: Date.now().toString(),
      name: name.trim(),
      calories: parseInt(calories) || 0,
      protein: parseInt(protein) || 0,
    };
    setMeals([newMeal, ...meals]);
    setName("");
    setCalories("");
    setProtein("");
  };

  const handleDeleteMeal = (id: string) => {
    setMeals(meals.filter((m) => m.id !== id));
  };

  // 👈 Tapping meal card navigates to /meal/[id] with typed parameters
  const handleMealPress = (meal: MealItem) => {
    router.push({
      pathname: "/meal/[id]",
      params: {
        id: meal.id,
        name: meal.name,
        calories: meal.calories.toString(),
        protein: meal.protein.toString(),
      },
    });
  };

  return (
    <SafeAreaView className="flex-1 bg-slate-950 px-5 pt-2">
      
      {/* ── Top Dashboard Header & Progress ── */}
      <View className="bg-slate-900 border border-slate-800 rounded-3xl p-5 mb-4 shadow-2xl">
        <View className="flex-row items-center justify-between mb-3">
          <View>
            <Text className="text-xs font-bold uppercase tracking-wider text-indigo-400">
              Daily Progress
            </Text>
            <Text className="text-2xl font-bold text-white">MacroZone Tracker</Text>
          </View>

          {/* 👈 Declarative Link to Modal Sheet */}
          <Link href="/add-meal" asChild>
            <Pressable className="bg-indigo-600 active:bg-indigo-700 px-3.5 py-2 rounded-2xl">
              <Text className="text-white text-xs font-bold">+ Modal</Text>
            </Pressable>
          </Link>
        </View>

        <View className="flex-row gap-3">
          <View className="flex-1 bg-slate-950/80 border border-slate-800 p-3 rounded-2xl">
            <Text className="text-slate-400 text-xs">Total Calories</Text>
            <Text className="text-white text-xl font-bold mt-1">{totalCalories} kcal</Text>
          </View>
          <View className="flex-1 bg-slate-950/80 border border-slate-800 p-3 rounded-2xl">
            <Text className="text-slate-400 text-xs">Total Protein</Text>
            <Text className="text-white text-xl font-bold mt-1">{totalProtein} g</Text>
          </View>
        </View>
      </View>

      {/* ── Quick Add Form ── */}
      <View className="bg-slate-900/90 border border-slate-800 p-4 rounded-2xl mb-4 gap-2.5">
        <TextInput
          value={name}
          onChangeText={setName}
          placeholder="Quick Food Name (e.g. Protein Shake)"
          placeholderTextColor="#64748b"
          className="bg-slate-950 border border-slate-800 text-white px-3.5 py-2.5 rounded-xl text-sm"
        />
        <View className="flex-row gap-2.5">
          <TextInput
            value={calories}
            onChangeText={setCalories}
            placeholder="Calories (kcal)"
            placeholderTextColor="#64748b"
            keyboardType="numeric"
            className="flex-1 bg-slate-950 border border-slate-800 text-white px-3.5 py-2.5 rounded-xl text-sm"
          />
          <TextInput
            value={protein}
            onChangeText={setProtein}
            placeholder="Protein (g)"
            placeholderTextColor="#64748b"
            keyboardType="numeric"
            className="flex-1 bg-slate-950 border border-slate-800 text-white px-3.5 py-2.5 rounded-xl text-sm"
          />
        </View>
        <Pressable
          onPress={handleAddMeal}
          className="bg-indigo-600 active:bg-indigo-700 py-3 rounded-xl items-center mt-1"
        >
          <Text className="text-white font-semibold text-sm">+ Quick Add Meal</Text>
        </Pressable>
      </View>

      {/* ── Clickable Meal FlatList ── */}
      <FlatList
        data={meals}
        keyExtractor={(item) => item.id}
        showsVerticalScrollIndicator={false}
        renderItem={({ item }) => (
          <Pressable
            onPress={() => handleMealPress(item)}
            className="bg-slate-900 border border-slate-800/80 p-3.5 rounded-2xl flex-row justify-between items-center mb-2.5 active:bg-slate-800"
          >
            <View className="flex-1 mr-2">
              <Text className="text-white font-medium text-sm">{item.name}</Text>
              <Text className="text-slate-400 text-xs mt-0.5">Protein: {item.protein}g</Text>
            </View>
            <View className="flex-row items-center gap-2">
              <View className="bg-indigo-500/10 border border-indigo-500/20 px-2.5 py-1 rounded-xl">
                <Text className="text-indigo-300 font-bold text-xs">{item.calories} kcal</Text>
              </View>
              <Pressable
                onPress={(e) => {
                  e.stopPropagation?.();
                  handleDeleteMeal(item.id);
                }}
                className="w-7 h-7 rounded-lg bg-red-500/10 border border-red-500/20 items-center justify-center active:bg-red-500/20"
              >
                <Text className="text-red-400 text-xs font-bold">✕</Text>
              </Pressable>
            </View>
          </Pressable>
        )}
      />
    </SafeAreaView>
  );
}
app/meal/[id].tsx (Dynamic Nutrition Breakdown Screen)
import { useLocalSearchParams, useRouter } from "expo-router";
import { View, Text, Pressable } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";

export default function MealDetailScreen() {
  const router = useRouter(); // 👈 Back navigation ke liye
  
  // 👈 Extract parameters passed from list
  const { id, name, calories, protein } = useLocalSearchParams<{
    id: string;
    name: string;
    calories: string;
    protein: string;
  }>();

  const calNum = parseInt(calories || "0") || 0;
  const proNum = parseInt(protein || "0") || 0;
  const carbsNum = Math.max(0, Math.round(calNum * 0.12));
  const fatsNum = Math.max(0, Math.round((calNum - proNum * 4 - carbsNum * 4) / 9));

  return (
    <SafeAreaView className="flex-1 bg-slate-950 px-5 pt-2">
      {/* 👈 Top Navigation Bar */}
      <View className="flex-row items-center justify-between mb-6">
        <Pressable
          onPress={() => router.back()} // 👈 Pops screen from stack
          className="bg-slate-900 border border-slate-800 px-4 py-2.5 rounded-2xl active:bg-slate-800"
        >
          <Text className="text-indigo-400 font-bold text-sm">← Back</Text>
        </Pressable>
        <Text className="text-slate-400 text-xs font-mono">ID: #{id}</Text>
      </View>

      {/* 👈 Hero Nutrition Card */}
      <View className="bg-slate-900 border border-slate-800 p-6 rounded-3xl mb-5 shadow-2xl">
        <Text className="text-xs font-bold text-indigo-400 uppercase tracking-wider mb-1">
          Meal Breakdown
        </Text>
        <Text className="text-2xl font-extrabold text-white mb-4">{name}</Text>

        <View className="flex-row gap-3 mb-5">
          <View className="flex-1 bg-slate-950/80 border border-slate-800 p-4 rounded-2xl">
            <Text className="text-slate-400 text-xs">Calories</Text>
            <Text className="text-white text-2xl font-bold mt-1">{calNum} kcal</Text>
          </View>
          <View className="flex-1 bg-slate-950/80 border border-slate-800 p-4 rounded-2xl">
            <Text className="text-slate-400 text-xs">Protein</Text>
            <Text className="text-emerald-400 text-2xl font-bold mt-1">{proNum} g</Text>
          </View>
        </View>

        <View className="gap-2">
          <View className="flex-row justify-between text-xs">
            <Text className="text-slate-400 text-xs">Estimated Carbs</Text>
            <Text className="text-white font-semibold text-xs">{carbsNum}g</Text>
          </View>
          <View className="flex-row justify-between text-xs">
            <Text className="text-slate-400 text-xs">Estimated Fats</Text>
            <Text className="text-white font-semibold text-xs">{fatsNum}g</Text>
          </View>
        </View>
      </View>

      <Pressable
        onPress={() => router.back()}
        className="bg-indigo-600 active:bg-indigo-700 py-3.5 rounded-2xl items-center"
      >
        <Text className="text-white font-bold text-sm">Return to Dashboard</Text>
      </Pressable>
    </SafeAreaView>
  );
}

6. Interactive Live Knowledge Checks

Test your understanding of Expo Router, stack navigation, and parameters with these 4 interactive challenges:

Check 1: How do you define a Dynamic Route file for a meal ID in Expo Router?

Check 2: Which hook is used inside a dynamic screen to read URL route parameters?

Check 3: What does the router.back() method do in a Stack navigator?

Check 4: Which Stack.Screen option makes a screen slide up from the bottom as a sheet?

🛠️ Live Teacher Coding Challenge

Let's test our navigation architecture in your real app workspace:

  1. Open app/_layout.tsx — examine how the dark Stack navigator and modal options are defined.
  2. Open app/index.tsx — notice how tapping any meal card calls router.push() with dynamic parameters, and the "+ Modal" button opens /add-meal.
  3. Open app/meal/[id].tsx — see how useLocalSearchParams extracts the data and displays it with a back button.
  4. Test navigating between all three screens on your phone with Expo Go!

7. Primary Sources & Reference Material

← Lesson 02: Dynamic Lists & Forms Reference Sheet 03 →

💬 Ask Your Teacher: What questions do you have about router.push(), stack transitions, or passing parameters? Let's discuss in the chat!