Mastering File-Based Routing, Stack Navigators, Dynamic Route Segments ([id]), useLocalSearchParams, and Native Screen Transitions.
Altamash, mobile navigation ko master karne ke liye ye 3 step-by-step mental models samajhna zaroori hai:
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.
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.
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.
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.
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.
app/_layout.tsx → Root Stack NavigatorSets common dark theme background, header colors, and screen transition rules.
app/index.tsx/ (Home Screen)app/details.tsx/details (Detail Screen)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>
);
}
| 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. |
useRouter() & <Link>To move between screens, React Native gives you two ways:
useRouter() (Imperative): Button tap ya function ke andar code se navigate karna (e.g. router.push('/details') ya router.back()).<Link> (Declarative): JSX tag ke roop me link banana (web ke <a> tag jaisa).Naya screen stack ke upar slide ho jata hai.
Top screen remove ho jata hai aur purana screen visible hota hai.
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>
);
}
| 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. |
[id].tsx) & Passing Parameters (useLocalSearchParams)Jab kisi list ke item par click karke uski individual details dikhani ho, hum Dynamic Routes use karte hain:
app/meal/[id].tsx.router.push({ pathname: '/meal/[id]', params: { id: '1', name: 'Oats' } }).useLocalSearchParams() hook se data receive karte hain.router.push({ pathname: "/meal/[id]", params: { id: "1", name: "Oats", calories: "420" } })
app/meal/[id].tsxconst { id, name, calories } = useLocalSearchParams()
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>
);
}
| 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. |
Here is the visual cheat sheet for all Tailwind utility classes used in our navigation layouts:
flex-1 = Full screen height & width stretch (flex: 1 1 0%).flex-row = Arrange children side-by-side horizontally (flex-direction: row).justify-between = Push left and right elements to opposite screen edges (justify-content: space-between).items-center = Vertically center child items along the cross axis (align-items: center).gap-3 = 12px spacing between child elements (3 × 4px = 12px).bg-slate-950 = Deep OLED pitch-black background (#020617).bg-slate-900 = Elevated card surface background (#0f172a).border-slate-800 = Subtle 1px dark border divider (#1e293b).rounded-2xl / rounded-3xl = 16px & 24px modern curved border radius.active:bg-slate-800 / active:bg-indigo-700 = Darkens on touch for instant tactile native feedback.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!
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>
);
}
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>
);
}
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>
);
}
Test your understanding of Expo Router, stack navigation, and parameters with these 4 interactive challenges:
router.back() method do in a Stack navigator?Let's test our navigation architecture in your real app workspace:
router.push() with dynamic parameters, and the "+ Modal" button opens /add-meal.useLocalSearchParams extracts the data and displays it with a back button.💬 Ask Your Teacher: What questions do you have about router.push(), stack transitions, or passing parameters? Let's discuss in the chat!