Module Overview
Estimated Time: 4 hours | Difficulty: Intermediate | Prerequisites: Core Components, Styling
- React Navigation setup
- Stack Navigator
- Tab Navigator
- Drawer Navigator
- Passing parameters
- Navigation TypeScript types
Navigation Architecture
Copy
┌─────────────────────────────────────────────────────────────────────────────┐
│ React Navigation Architecture │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ NavigationContainer │
│ └── Root Navigator (Stack) │
│ ├── Auth Stack │
│ │ ├── Login Screen │
│ │ ├── Register Screen │
│ │ └── Forgot Password Screen │
│ │ │
│ └── Main Stack │
│ ├── Tab Navigator │
│ │ ├── Home Tab │
│ │ │ └── Home Stack │
│ │ │ ├── Home Screen │
│ │ │ └── Details Screen │
│ │ ├── Search Tab │
│ │ ├── Notifications Tab │
│ │ └── Profile Tab │
│ │ │
│ └── Modal Screens │
│ ├── Settings Modal │
│ └── Create Post Modal │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Installation & Setup
Copy
# Core packages
npm install @react-navigation/native
# Dependencies for Expo
npx expo install react-native-screens react-native-safe-area-context
# Navigator packages
npm install @react-navigation/native-stack
npm install @react-navigation/bottom-tabs
npm install @react-navigation/drawer
# For drawer (additional dependency)
npx expo install react-native-gesture-handler react-native-reanimated
Basic Setup
Copy
// App.tsx
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { SafeAreaProvider } from 'react-native-safe-area-context';
const Stack = createNativeStackNavigator();
export default function App() {
return (
<SafeAreaProvider>
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
</SafeAreaProvider>
);
}
Stack Navigator
Basic Stack Navigation
Copy
// navigation/stacks/HomeStack.tsx
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { HomeScreen } from '@/screens/HomeScreen';
import { DetailsScreen } from '@/screens/DetailsScreen';
import { ProductScreen } from '@/screens/ProductScreen';
export type HomeStackParamList = {
Home: undefined;
Details: { id: string; title: string };
Product: { productId: string };
};
const Stack = createNativeStackNavigator<HomeStackParamList>();
export function HomeStack() {
return (
<Stack.Navigator
initialRouteName="Home"
screenOptions={{
headerStyle: {
backgroundColor: '#3b82f6',
},
headerTintColor: '#fff',
headerTitleStyle: {
fontWeight: '600',
},
headerBackTitleVisible: false,
animation: 'slide_from_right',
}}
>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{
title: 'Home',
headerLargeTitle: true,
}}
/>
<Stack.Screen
name="Details"
component={DetailsScreen}
options={({ route }) => ({
title: route.params.title,
})}
/>
<Stack.Screen
name="Product"
component={ProductScreen}
options={{
presentation: 'modal',
animation: 'slide_from_bottom',
}}
/>
</Stack.Navigator>
);
}
Navigating Between Screens
Copy
// screens/HomeScreen.tsx
import { View, Text, Pressable, FlatList, StyleSheet } from 'react-native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { HomeStackParamList } from '@/navigation/stacks/HomeStack';
type Props = NativeStackScreenProps<HomeStackParamList, 'Home'>;
const items = [
{ id: '1', title: 'First Item' },
{ id: '2', title: 'Second Item' },
{ id: '3', title: 'Third Item' },
];
export function HomeScreen({ navigation }: Props) {
return (
<View style={styles.container}>
<FlatList
data={items}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<Pressable
style={styles.item}
onPress={() => navigation.navigate('Details', {
id: item.id,
title: item.title,
})}
>
<Text style={styles.itemText}>{item.title}</Text>
</Pressable>
)}
/>
{/* Navigate with push (allows duplicate screens) */}
<Pressable
style={styles.button}
onPress={() => navigation.push('Details', { id: '1', title: 'Pushed' })}
>
<Text style={styles.buttonText}>Push Details</Text>
</Pressable>
</View>
);
}
// screens/DetailsScreen.tsx
type DetailsProps = NativeStackScreenProps<HomeStackParamList, 'Details'>;
export function DetailsScreen({ route, navigation }: DetailsProps) {
const { id, title } = route.params;
return (
<View style={styles.container}>
<Text style={styles.title}>{title}</Text>
<Text style={styles.subtitle}>ID: {id}</Text>
{/* Go back */}
<Pressable style={styles.button} onPress={() => navigation.goBack()}>
<Text style={styles.buttonText}>Go Back</Text>
</Pressable>
{/* Go to specific screen */}
<Pressable
style={styles.button}
onPress={() => navigation.navigate('Home')}
>
<Text style={styles.buttonText}>Go to Home</Text>
</Pressable>
{/* Pop to top of stack */}
<Pressable
style={styles.button}
onPress={() => navigation.popToTop()}
>
<Text style={styles.buttonText}>Pop to Top</Text>
</Pressable>
{/* Replace current screen */}
<Pressable
style={styles.button}
onPress={() => navigation.replace('Details', { id: '2', title: 'Replaced' })}
>
<Text style={styles.buttonText}>Replace Screen</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
item: {
padding: 16,
backgroundColor: '#f3f4f6',
borderRadius: 8,
marginBottom: 8,
},
itemText: {
fontSize: 16,
color: '#111827',
},
title: {
fontSize: 24,
fontWeight: 'bold',
marginBottom: 8,
},
subtitle: {
fontSize: 16,
color: '#6b7280',
marginBottom: 24,
},
button: {
backgroundColor: '#3b82f6',
padding: 16,
borderRadius: 8,
alignItems: 'center',
marginBottom: 12,
},
buttonText: {
color: '#fff',
fontWeight: '600',
},
});
Tab Navigator
Bottom Tab Navigation
Copy
// navigation/tabs/MainTabs.tsx
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import { HomeStack } from '../stacks/HomeStack';
import { SearchScreen } from '@/screens/SearchScreen';
import { NotificationsScreen } from '@/screens/NotificationsScreen';
import { ProfileScreen } from '@/screens/ProfileScreen';
export type MainTabParamList = {
HomeTab: undefined;
Search: undefined;
Notifications: undefined;
Profile: undefined;
};
const Tab = createBottomTabNavigator<MainTabParamList>();
export function MainTabs() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName: keyof typeof Ionicons.glyphMap;
switch (route.name) {
case 'HomeTab':
iconName = focused ? 'home' : 'home-outline';
break;
case 'Search':
iconName = focused ? 'search' : 'search-outline';
break;
case 'Notifications':
iconName = focused ? 'notifications' : 'notifications-outline';
break;
case 'Profile':
iconName = focused ? 'person' : 'person-outline';
break;
default:
iconName = 'help-outline';
}
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#3b82f6',
tabBarInactiveTintColor: '#9ca3af',
tabBarStyle: {
backgroundColor: '#fff',
borderTopWidth: 1,
borderTopColor: '#e5e7eb',
paddingBottom: 8,
paddingTop: 8,
height: 60,
},
tabBarLabelStyle: {
fontSize: 12,
fontWeight: '500',
},
headerShown: false,
})}
>
<Tab.Screen
name="HomeTab"
component={HomeStack}
options={{
tabBarLabel: 'Home',
}}
/>
<Tab.Screen
name="Search"
component={SearchScreen}
options={{
headerShown: true,
headerTitle: 'Search',
}}
/>
<Tab.Screen
name="Notifications"
component={NotificationsScreen}
options={{
headerShown: true,
headerTitle: 'Notifications',
tabBarBadge: 3, // Show badge
}}
/>
<Tab.Screen
name="Profile"
component={ProfileScreen}
options={{
headerShown: true,
headerTitle: 'Profile',
}}
/>
</Tab.Navigator>
);
}
Custom Tab Bar
Copy
// components/navigation/CustomTabBar.tsx
import { View, Pressable, StyleSheet } from 'react-native';
import { BottomTabBarProps } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import Animated, {
useAnimatedStyle,
withSpring,
interpolateColor,
} from 'react-native-reanimated';
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
export function CustomTabBar({ state, descriptors, navigation }: BottomTabBarProps) {
return (
<View style={styles.container}>
{state.routes.map((route, index) => {
const { options } = descriptors[route.key];
const isFocused = state.index === index;
const onPress = () => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (!isFocused && !event.defaultPrevented) {
navigation.navigate(route.name);
}
};
return (
<TabButton
key={route.key}
routeName={route.name}
isFocused={isFocused}
onPress={onPress}
/>
);
})}
</View>
);
}
interface TabButtonProps {
routeName: string;
isFocused: boolean;
onPress: () => void;
}
function TabButton({ routeName, isFocused, onPress }: TabButtonProps) {
const iconMap: Record<string, keyof typeof Ionicons.glyphMap> = {
HomeTab: 'home',
Search: 'search',
Notifications: 'notifications',
Profile: 'person',
};
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: withSpring(isFocused ? 1.1 : 1) }],
backgroundColor: interpolateColor(
isFocused ? 1 : 0,
[0, 1],
['transparent', '#eff6ff']
),
}));
return (
<AnimatedPressable
style={[styles.tabButton, animatedStyle]}
onPress={onPress}
>
<Ionicons
name={isFocused ? iconMap[routeName] : `${iconMap[routeName]}-outline` as any}
size={24}
color={isFocused ? '#3b82f6' : '#9ca3af'}
/>
</AnimatedPressable>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
backgroundColor: '#fff',
paddingVertical: 12,
paddingHorizontal: 16,
borderTopWidth: 1,
borderTopColor: '#e5e7eb',
},
tabButton: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 8,
borderRadius: 12,
},
});
Drawer Navigator
Copy
// navigation/drawers/MainDrawer.tsx
import { createDrawerNavigator, DrawerContentScrollView, DrawerItemList, DrawerContentComponentProps } from '@react-navigation/drawer';
import { View, Text, Image, StyleSheet, Pressable } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { MainTabs } from '../tabs/MainTabs';
import { SettingsScreen } from '@/screens/SettingsScreen';
import { HelpScreen } from '@/screens/HelpScreen';
import { useAuth } from '@/hooks/useAuth';
export type DrawerParamList = {
Main: undefined;
Settings: undefined;
Help: undefined;
};
const Drawer = createDrawerNavigator<DrawerParamList>();
export function MainDrawer() {
return (
<Drawer.Navigator
drawerContent={(props) => <CustomDrawerContent {...props} />}
screenOptions={{
headerShown: false,
drawerActiveBackgroundColor: '#eff6ff',
drawerActiveTintColor: '#3b82f6',
drawerInactiveTintColor: '#6b7280',
drawerLabelStyle: {
fontSize: 16,
fontWeight: '500',
},
}}
>
<Drawer.Screen
name="Main"
component={MainTabs}
options={{
drawerLabel: 'Home',
drawerIcon: ({ color, size }) => (
<Ionicons name="home-outline" size={size} color={color} />
),
}}
/>
<Drawer.Screen
name="Settings"
component={SettingsScreen}
options={{
headerShown: true,
drawerIcon: ({ color, size }) => (
<Ionicons name="settings-outline" size={size} color={color} />
),
}}
/>
<Drawer.Screen
name="Help"
component={HelpScreen}
options={{
headerShown: true,
drawerIcon: ({ color, size }) => (
<Ionicons name="help-circle-outline" size={size} color={color} />
),
}}
/>
</Drawer.Navigator>
);
}
function CustomDrawerContent(props: DrawerContentComponentProps) {
const { user, logout } = useAuth();
return (
<DrawerContentScrollView {...props}>
{/* User Profile Section */}
<View style={styles.profileSection}>
<Image
source={{ uri: user?.avatar || 'https://via.placeholder.com/80' }}
style={styles.avatar}
/>
<Text style={styles.userName}>{user?.name || 'Guest'}</Text>
<Text style={styles.userEmail}>{user?.email || ''}</Text>
</View>
{/* Navigation Items */}
<DrawerItemList {...props} />
{/* Logout Button */}
<View style={styles.footer}>
<Pressable style={styles.logoutButton} onPress={logout}>
<Ionicons name="log-out-outline" size={24} color="#ef4444" />
<Text style={styles.logoutText}>Logout</Text>
</Pressable>
</View>
</DrawerContentScrollView>
);
}
const styles = StyleSheet.create({
profileSection: {
padding: 20,
borderBottomWidth: 1,
borderBottomColor: '#e5e7eb',
marginBottom: 10,
},
avatar: {
width: 80,
height: 80,
borderRadius: 40,
marginBottom: 12,
},
userName: {
fontSize: 18,
fontWeight: '600',
color: '#111827',
},
userEmail: {
fontSize: 14,
color: '#6b7280',
marginTop: 4,
},
footer: {
padding: 20,
borderTopWidth: 1,
borderTopColor: '#e5e7eb',
marginTop: 'auto',
},
logoutButton: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
},
logoutText: {
fontSize: 16,
color: '#ef4444',
fontWeight: '500',
},
});
TypeScript Navigation Types
Centralized Type Definitions
Copy
// navigation/types.ts
import { NavigatorScreenParams, CompositeScreenProps } from '@react-navigation/native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { BottomTabScreenProps } from '@react-navigation/bottom-tabs';
import { DrawerScreenProps } from '@react-navigation/drawer';
// Root Stack
export type RootStackParamList = {
Auth: NavigatorScreenParams<AuthStackParamList>;
Main: NavigatorScreenParams<DrawerParamList>;
};
// Auth Stack
export type AuthStackParamList = {
Login: undefined;
Register: undefined;
ForgotPassword: { email?: string };
};
// Drawer
export type DrawerParamList = {
Tabs: NavigatorScreenParams<MainTabParamList>;
Settings: undefined;
Help: undefined;
};
// Main Tabs
export type MainTabParamList = {
HomeTab: NavigatorScreenParams<HomeStackParamList>;
Search: undefined;
Notifications: undefined;
Profile: undefined;
};
// Home Stack
export type HomeStackParamList = {
Home: undefined;
Details: { id: string; title: string };
Product: { productId: string };
};
// Screen Props Types
export type RootStackScreenProps<T extends keyof RootStackParamList> =
NativeStackScreenProps<RootStackParamList, T>;
export type AuthStackScreenProps<T extends keyof AuthStackParamList> =
CompositeScreenProps<
NativeStackScreenProps<AuthStackParamList, T>,
RootStackScreenProps<keyof RootStackParamList>
>;
export type MainTabScreenProps<T extends keyof MainTabParamList> =
CompositeScreenProps<
BottomTabScreenProps<MainTabParamList, T>,
DrawerScreenProps<DrawerParamList>
>;
export type HomeStackScreenProps<T extends keyof HomeStackParamList> =
CompositeScreenProps<
NativeStackScreenProps<HomeStackParamList, T>,
MainTabScreenProps<keyof MainTabParamList>
>;
// Declare global types for useNavigation hook
declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}
Using Typed Navigation
Copy
// screens/HomeScreen.tsx
import { HomeStackScreenProps } from '@/navigation/types';
type Props = HomeStackScreenProps<'Home'>;
export function HomeScreen({ navigation, route }: Props) {
// Fully typed navigation
const goToDetails = () => {
navigation.navigate('Details', { id: '1', title: 'Item' }); // ✅ Type-safe
// navigation.navigate('Details', { id: 1 }); // ❌ Error: id must be string
};
// Navigate to other stacks
const goToProfile = () => {
navigation.navigate('Profile'); // Navigate to tab
};
return (
// ...
);
}
useNavigation Hook with Types
Copy
// hooks/useTypedNavigation.ts
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { RootStackParamList } from '@/navigation/types';
export function useTypedNavigation() {
return useNavigation<NativeStackNavigationProp<RootStackParamList>>();
}
// Usage in any component
function SomeComponent() {
const navigation = useTypedNavigation();
// Fully typed
navigation.navigate('Main', { screen: 'Tabs', params: { screen: 'HomeTab' } });
}
Navigation Patterns
Authentication Flow
Copy
// navigation/RootNavigator.tsx
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useAuth } from '@/hooks/useAuth';
import { AuthStack } from './stacks/AuthStack';
import { MainDrawer } from './drawers/MainDrawer';
import { LoadingScreen } from '@/screens/LoadingScreen';
const Stack = createNativeStackNavigator<RootStackParamList>();
export function RootNavigator() {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return <LoadingScreen />;
}
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
{isAuthenticated ? (
<Stack.Screen name="Main" component={MainDrawer} />
) : (
<Stack.Screen name="Auth" component={AuthStack} />
)}
</Stack.Navigator>
);
}
Deep Linking
Copy
// navigation/linking.ts
import { LinkingOptions } from '@react-navigation/native';
import * as Linking from 'expo-linking';
import { RootStackParamList } from './types';
const prefix = Linking.createURL('/');
export const linking: LinkingOptions<RootStackParamList> = {
prefixes: [prefix, 'myapp://', 'https://myapp.com'],
config: {
screens: {
Auth: {
screens: {
Login: 'login',
Register: 'register',
ForgotPassword: 'forgot-password',
},
},
Main: {
screens: {
Tabs: {
screens: {
HomeTab: {
screens: {
Home: 'home',
Details: 'details/:id',
Product: 'product/:productId',
},
},
Search: 'search',
Notifications: 'notifications',
Profile: 'profile',
},
},
Settings: 'settings',
},
},
},
},
};
// App.tsx
<NavigationContainer linking={linking}>
<RootNavigator />
</NavigationContainer>
Best Practices
Type Everything
Use TypeScript for all navigation params and screen props
Organize by Feature
Group related screens and navigators together
Lazy Load Screens
Use React.lazy for screens not immediately needed
Handle Deep Links
Configure deep linking for better user experience
Next Steps
Module 9: Advanced Navigation
Learn advanced navigation patterns including nested navigators and custom transitions