Module Overview
Estimated Time: 3-4 hours | Difficulty: Intermediate | Prerequisites: Core Components
- 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
Navigation Options
Option 1: Expo Router (Recommended)
File-based routing inspired by Next.js:Copy
# Already included in new Expo projects
npx create-expo-app@latest --template tabs
Option 2: React Navigation
Traditional API-based navigation:Copy
# 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
Copy
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
Copy
// 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
Copy
// 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>
);
}
Navigation Between Screens
Copy
// 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
Copy
// 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
Copy
// 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
Copy
// 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
Copy
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
Copy
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:Copy
// 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
Copy
// app.json
{
"expo": {
"scheme": "myapp",
"web": {
"bundler": "metro"
}
}
}
myapp://user/123 will automatically route to /user/123.
React Navigation Deep Linking
Copy
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>
);
}
Navigation State & Events
useNavigationState
Copy
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
Copy
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
Copy
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
Copy
// 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 Patterns Cheat Sheet
Copy
┌──────────────────────────────────────────────────────────────────────┐
│ 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