React Native Navigation: Stack, Tab, and Drawer Explained
Navigation in React Native is not built-in the way routing is in web frameworks. You choose a library, and the de facto standard is React Navigation — a JavaScript-based navigation library that works on both iOS and Android. Understanding how to compose its three core navigators (Stack, Tab, Drawer) and how they nest together covers 95% of what any mobile app needs.
Installation
$ npm install @react-navigation/native
# Required peer dependencies
$ npm install react-native-screens react-native-safe-area-context
# On iOS, install native deps
$ cd ios && pod install
Then wrap your entire app in a NavigationContainer:
// App.tsx
import { NavigationContainer } from '@react-navigation/native';
export default function App() {
return (
<NavigationContainer>
{/* navigators go here */}
</NavigationContainer>
);
}
Stack Navigator
Stack Navigator mimics iOS navigation — screens slide in from the right; the back button pops them off. Install it:
$ npm install @react-navigation/stack
$ npm install react-native-gesture-handler # required dependency
import { createStackNavigator } from '@react-navigation/stack';
import HomeScreen from './screens/HomeScreen';
import DetailScreen from './screens/DetailScreen';
const Stack = createStackNavigator();
function AppStack() {
return (
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: 'My App' }}
/>
<Stack.Screen
name="Detail"
component={DetailScreen}
options={({ route }) => ({ title: route.params.title })}
/>
</Stack.Navigator>
);
}
Navigating Between Screens
Every screen component receives a navigation prop automatically:
// HomeScreen.tsx
function HomeScreen({ navigation }) {
return (
<View>
<Text>Home</Text>
<Button
title="Go to Detail"
onPress={() =>
navigation.navigate('Detail', { id: 42, title: 'Item 42' })
}
/>
</View>
);
}
Receiving Params
// DetailScreen.tsx
function DetailScreen({ route, navigation }) {
const { id, title } = route.params;
return (
<View>
<Text>ID: {id}</Text>
<Text>Title: {title}</Text>
<Button title="Go Back" onPress={() => navigation.goBack()} />
</View>
);
}
Stack Navigation Methods
navigation.navigate('ScreenName'); // go to screen
navigation.navigate('ScreenName', { key: 'value' }); // with params
navigation.push('ScreenName'); // push even if already in stack
navigation.goBack(); // pop current screen
navigation.popToTop(); // pop to first screen
navigation.replace('ScreenName'); // replace current screen
Customizing the Header
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={{
title: 'Profile',
headerStyle: { backgroundColor: '#6200ea' },
headerTintColor: '#fff',
headerRight: () => (
<Button title="Edit" onPress={() => alert('Edit pressed')} />
),
headerLeft: () => null, // remove back button
}}
/>
Bottom Tab Navigator
Tab Navigator shows a persistent tab bar at the bottom — the standard pattern for top-level navigation sections.
$ npm install @react-navigation/bottom-tabs
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Icon from 'react-native-vector-icons/MaterialIcons';
const Tab = createBottomTabNavigator();
function AppTabs() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
const icons = {
Home: 'home',
Search: 'search',
Profile: 'person',
};
return <Icon name={icons[route.name]} size={size} color={color} />;
},
tabBarActiveTintColor: '#6200ea',
tabBarInactiveTintColor: '#9e9e9e',
})}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Search" component={SearchScreen} />
<Tab.Screen
name="Profile"
component={ProfileScreen}
options={{ tabBarBadge: 3 }} // notification badge
/>
</Tab.Navigator>
);
}
Drawer Navigator
Drawer slides in from the left (or right) — common for settings, account switching, or secondary navigation sections.
$ npm install @react-navigation/drawer
$ npm install react-native-reanimated react-native-gesture-handler
import { createDrawerNavigator } from '@react-navigation/drawer';
const Drawer = createDrawerNavigator();
function AppDrawer() {
return (
<Drawer.Navigator
initialRouteName="Home"
screenOptions={{
drawerStyle: { backgroundColor: '#f5f5f5', width: 260 },
drawerActiveTintColor: '#6200ea',
}}
>
<Drawer.Screen name="Home" component={HomeScreen} />
<Drawer.Screen name="Settings" component={SettingsScreen} />
<Drawer.Screen name="About" component={AboutScreen} />
</Drawer.Navigator>
);
}
Open/close the drawer programmatically:
navigation.openDrawer();
navigation.closeDrawer();
navigation.toggleDrawer();
Nesting Navigators
Real apps need multiple navigator types together. The typical pattern is tabs at the top level, each tab containing a stack:
function HomeStack() {
return (
<Stack.Navigator>
<Stack.Screen name="HomeFeed" component={HomeFeedScreen} />
<Stack.Screen name="PostDetail" component={PostDetailScreen} />
</Stack.Navigator>
);
}
function ProfileStack() {
return (
<Stack.Navigator>
<Stack.Screen name="ProfileMain" component={ProfileMainScreen} />
<Stack.Screen name="EditProfile" component={EditProfileScreen} />
</Stack.Navigator>
);
}
function AppTabs() {
return (
<Tab.Navigator screenOptions={{ headerShown: false }}>
<Tab.Screen name="Home" component={HomeStack} />
<Tab.Screen name="Profile" component={ProfileStack} />
</Tab.Navigator>
);
}
// Root — tabs with optional full-screen modal on top
const RootStack = createStackNavigator();
function App() {
return (
<NavigationContainer>
<RootStack.Navigator screenOptions={{ headerShown: false }}>
<RootStack.Screen name="Main" component={AppTabs} />
<RootStack.Screen
name="Modal"
component={ModalScreen}
options={{ presentation: 'modal' }}
/>
</RootStack.Navigator>
</NavigationContainer>
);
}
headerShown: false on the Tab navigator prevents double headers — the Stack within each tab handles its own header.
TypeScript: Typed Navigation
// types/navigation.ts
export type RootStackParamList = {
Home: undefined;
Detail: { id: number; title: string };
Modal: { message: string };
};
export type TabParamList = {
Home: undefined;
Profile: undefined;
};
import { NativeStackScreenProps } from '@react-navigation/native-stack';
type Props = NativeStackScreenProps<RootStackParamList, 'Detail'>;
function DetailScreen({ route, navigation }: Props) {
const { id, title } = route.params; // fully typed
// ...
}
Useful Hooks
Outside of screen components, use hooks to access navigation:
import { useNavigation, useRoute, useFocusEffect } from '@react-navigation/native';
function SomeComponent() {
const navigation = useNavigation();
const route = useRoute();
// Runs when screen comes into focus
useFocusEffect(() => {
fetchFreshData();
return () => cancelFetch(); // cleanup when losing focus
});
return <Button title="Go Home" onPress={() => navigation.navigate('Home')} />;
}
Conclusion
React Navigation’s three navigators — Stack for hierarchical depth, Tab for top-level sections, Drawer for secondary navigation — cover every pattern you’ll encounter in mobile apps. Nest them together (tabs containing stacks is the most common combination), use screenOptions to set consistent styling, and add TypeScript types to your param lists from the start to avoid prop typo bugs. The useFocusEffect hook is particularly useful for refreshing data when a user returns to a screen after navigating away.