React Native Navigation Patterns
Navigation is the backbone of every mobile application. Unlike the web, where the browser handles URL-based routing automatically, React Native apps require you to explicitly define how users move between screens. React Navigation — the community's de facto standard library — gives you a rich, customizable, and performant navigation system that covers everything from simple stack-based flows to complex nested tab-and-drawer layouts. Understanding these patterns deeply will dramatically improve the architecture and user experience of your apps.
React Navigation is downloaded over 2 million times per week on npm and is the officially recommended navigation library in the React Native docs. Version 6 introduced a cleaner API with full TypeScript support, native stack integration via @react-navigation/native-stack, and dramatically better performance than JavaScript-only implementations.
Understanding React Navigation Architecture
Before diving into specific navigators, it helps to understand the mental model React Navigation uses. Every navigator maintains a navigation state — a plain JavaScript object describing which screens are currently mounted and which is focused. When you call navigation.navigate('Details'), React Navigation updates this state, causing the correct screen component to render.
The library is built around three core concepts:
- Navigators: Components that manage a set of screens and render the active one (Stack, Tab, Drawer)
- Screens: React components associated with a route name inside a navigator
- Navigation prop: Passed to every screen, giving it methods to navigate, go back, and read route params
Typical React Native navigation structure: Tab Navigator wrapping Stack Navigators
Installation and Setup
React Navigation is split into several packages so you only install what you need. The core package and native dependencies are required regardless of which navigator you use.
Install core dependencies
Install the main React Navigation package and the required native modules for gesture handling and safe area support.
# Core package and native dependencies
npm install @react-navigation/native
# Required peer dependencies
npm install react-native-screens react-native-safe-area-context
# For Expo projects, use:
npx expo install react-native-screens react-native-safe-area-context
Wrap your app in NavigationContainer
NavigationContainer manages the navigation tree and contains the navigation state. It must be the top-level component in your app.
import { NavigationContainer } from '@react-navigation/native';
export default function App() {
return (
<NavigationContainer>
{/* Your navigators go here */}
</NavigationContainer>
);
}
Install your chosen navigator packages
Each navigator type is a separate package. Install only the ones you need to keep your bundle size lean.
# Native Stack Navigator (recommended — uses native navigation primitives)
npm install @react-navigation/native-stack
# Bottom Tab Navigator
npm install @react-navigation/bottom-tabs
# Drawer Navigator
npm install @react-navigation/drawer
# Drawer also needs gesture handler and reanimated:
npm install react-native-gesture-handler react-native-reanimated
Prefer @react-navigation/native-stack over @react-navigation/stack. The native stack uses platform-native navigation components (UINavigationController on iOS, Fragment on Android) giving you genuine native transitions, better performance, and correct hardware back-button handling for free.
Stack Navigator: Screens as a Pile
The Stack Navigator is the most fundamental pattern in mobile navigation. Think of it as a stack of cards — each time you navigate to a new screen it's pushed on top, and going back pops it off. This gives users a clear sense of depth and history.
Basic Stack Setup
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import HomeScreen from '../screens/HomeScreen';
import DetailsScreen from '../screens/DetailsScreen';
import ProfileScreen from '../screens/ProfileScreen';
const Stack = createNativeStackNavigator();
export default function AppStack() {
return (
<Stack.Navigator
initialRouteName="Home"
screenOptions={{
headerStyle: { backgroundColor: '#6c5ce7' },
headerTintColor: '#fff',
headerTitleStyle: { fontWeight: '700' },
}}
>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: 'My App' }}
/>
<Stack.Screen
name="Details"
component={DetailsScreen}
options={({ route }) => ({ title: route.params?.title ?? 'Details' })}
/>
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={{ presentation: 'modal' }} // iOS modal presentation
/>
</Stack.Navigator>
);
}
Navigating Between Screens
Every screen component receives a navigation prop automatically. Use it to push new screens, replace the current one, or go back.
import { View, Text, Button, StyleSheet } from 'react-native';
export default function HomeScreen({ navigation }) {
return (
<View style={styles.container}>
<Text style={styles.title}>Welcome Home!</Text>
{/* Push a new screen onto the stack */}
<Button
title="Go to Details"
onPress={() => navigation.navigate('Details', { itemId: 42, title: 'Item 42' })}
/>
{/* Replace the current screen (no back button) */}
<Button
title="Replace with Profile"
onPress={() => navigation.replace('Profile')}
/>
{/* Push always adds a new screen, even if it already exists in the stack */}
<Button
title="Push Details Again"
onPress={() => navigation.push('Details', { itemId: 99, title: 'Item 99' })}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 },
title: { fontSize: 24, fontWeight: '700' },
});
navigation.navigate('Details') will go to an existing Details screen in the stack if one already exists (preventing duplicate screens). navigation.push('Details') always adds a fresh instance. Use navigate for standard flows and push when you explicitly need duplicate screens (e.g., a nested category explorer).
Tab Navigator: Parallel Screen Flows
Tab navigators are perfect for top-level navigation where all sections are equally important — think Instagram (Feed, Search, Reels, Shop, Profile). Each tab maintains its own navigation state independently, so switching tabs doesn't lose your scroll position or stack history.
Bottom Tab Navigator
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons'; // or react-native-vector-icons
import HomeStack from './HomeStack';
import SearchScreen from '../screens/SearchScreen';
import ProfileScreen from '../screens/ProfileScreen';
const Tab = createBottomTabNavigator();
export default function MainTabs() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
const icons = {
Home: focused ? 'home' : 'home-outline',
Search: focused ? 'search' : 'search-outline',
Profile: focused ? 'person' : 'person-outline',
};
return <Ionicons name={icons[route.name]} size={size} color={color} />;
},
tabBarActiveTintColor: '#6c5ce7',
tabBarInactiveTintColor: '#888',
tabBarStyle: {
backgroundColor: '#111',
borderTopColor: '#2a2a2a',
paddingBottom: 4,
},
headerShown: false, // Each stack manages its own header
})}
>
<Tab.Screen name="Home" component={HomeStack} />
<Tab.Screen name="Search" component={SearchScreen} />
<Tab.Screen
name="Profile"
component={ProfileScreen}
options={{
tabBarBadge: 3, // Show notification badge
}}
/>
</Tab.Navigator>
);
}
Tab Navigator Comparison
| Feature | Bottom Tabs | Material Top Tabs | Material Bottom Tabs |
|---|---|---|---|
| Position | Bottom of screen | Top (below header) | Bottom of screen |
| Swipe between tabs | No | Yes (ViewPager) | No |
| Best for | iOS-style apps, main nav | Content categories, settings | Material Design apps |
| Package | @react-navigation/bottom-tabs |
@react-navigation/material-top-tabs |
@react-navigation/material-bottom-tabs |
| Lazy loading | Yes (default) | Configurable | Yes (default) |
Drawer Navigator: Side Menu Patterns
The Drawer Navigator slides a menu in from the side — a common pattern for apps with many top-level destinations that don't all fit in a tab bar, or for settings, account options, and secondary navigation.
Basic Drawer Setup
import { createDrawerNavigator } from '@react-navigation/drawer';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import HomeScreen from '../screens/HomeScreen';
import SettingsScreen from '../screens/SettingsScreen';
import HelpScreen from '../screens/HelpScreen';
const Drawer = createDrawerNavigator();
export default function AppDrawer() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Drawer.Navigator
initialRouteName="Home"
screenOptions={{
drawerStyle: { backgroundColor: '#111', width: 280 },
drawerActiveTintColor: '#6c5ce7',
drawerInactiveTintColor: '#888',
drawerLabelStyle: { fontSize: 15 },
headerStyle: { backgroundColor: '#111' },
headerTintColor: '#fff',
}}
>
<Drawer.Screen
name="Home"
component={HomeScreen}
options={{ drawerIcon: ({ color }) => <Ionicons name="home" color={color} size={22} /> }}
/>
<Drawer.Screen
name="Settings"
component={SettingsScreen}
options={{ drawerIcon: ({ color }) => <Ionicons name="settings" color={color} size={22} /> }}
/>
<Drawer.Screen
name="Help"
component={HelpScreen}
options={{ drawerIcon: ({ color }) => <Ionicons name="help-circle" color={color} size={22} /> }}
/>
</Drawer.Navigator>
</GestureHandlerRootView>
);
}
Custom Drawer Content
For a more branded experience, replace the default drawer with a fully custom component using the drawerContent prop.
import { View, Text, Image, TouchableOpacity, StyleSheet } from 'react-native';
import { DrawerContentScrollView, DrawerItemList } from '@react-navigation/drawer';
export default function CustomDrawer(props) {
return (
<DrawerContentScrollView {...props} contentContainerStyle={styles.container}>
{/* User profile section */}
<View style={styles.profileSection}>
<Image
source={{ uri: 'https://example.com/avatar.jpg' }}
style={styles.avatar}
/>
<Text style={styles.name}>Mayur Dabhi</Text>
<Text style={styles.email}>mayurdabhi.6@gmail.com</Text>
</View>
{/* Default drawer items */}
<DrawerItemList {...props} />
{/* Custom footer item */}
<TouchableOpacity
style={styles.logoutBtn}
onPress={() => console.log('Logout')}
>
<Text style={styles.logoutText}>Log Out</Text>
</TouchableOpacity>
</DrawerContentScrollView>
);
}
// In your Drawer.Navigator:
// <Drawer.Navigator drawerContent={(props) => <CustomDrawer {...props} />}>
The Drawer Navigator requires react-native-gesture-handler. You must wrap your root component in <GestureHandlerRootView style={{ flex: 1 }}>. Forgetting this causes a silent failure where swipe-to-open gestures simply don't work — a common source of confusion for developers new to the library.
Nesting Navigators
Real-world apps rarely use a single navigator type. The most common pattern is tabs containing stacks: each tab gets its own independent stack navigator so users can drill into content within a tab without losing the tab bar.
Tab + Stack Pattern
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import HomeScreen from '../screens/HomeScreen';
import ArticleScreen from '../screens/ArticleScreen';
import CommentsScreen from '../screens/CommentsScreen';
const Stack = createNativeStackNavigator();
// Each tab gets its own stack navigator
export default function HomeStack() {
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
<Stack.Screen name="HomeMain" component={HomeScreen} />
<Stack.Screen name="Article" component={ArticleScreen} />
<Stack.Screen name="Comments" component={CommentsScreen} />
</Stack.Navigator>
);
}
// Then in MainTabs:
// <Tab.Screen name="Home" component={HomeStack} />
Navigating Across Navigators
Navigating to a screen in a sibling tab's stack requires a two-step navigate call. React Navigation handles the nesting automatically.
// Navigate to a screen in a different tab's stack:
navigation.navigate('ProfileTab', {
screen: 'EditProfile',
params: { userId: 123 },
});
// Navigate back to the root of the current stack:
navigation.popToTop();
// Navigate to a specific screen anywhere in the tree:
navigation.navigate('Root', {
screen: 'Home',
params: {
screen: 'Article',
params: { articleId: 42 },
},
});
// Reset the entire navigation state (e.g., after logout):
import { CommonActions } from '@react-navigation/native';
navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: 'Login' }],
})
);
Passing Params Between Screens
Params are how you pass data from one screen to the next. They travel with the route and are always available via route.params in the receiving screen.
Sending and Receiving Params
// --- SENDING PARAMS ---
// Pass an object as the second argument to navigate()
navigation.navigate('ProductDetails', {
productId: 'prod-123',
productName: 'Wireless Headphones',
price: 79.99,
});
// --- RECEIVING PARAMS ---
export default function ProductDetailsScreen({ route, navigation }) {
const { productId, productName, price } = route.params;
return (
<View>
<Text>{productName}</Text>
<Text>${price}</Text>
<Button
title="Add to Cart"
onPress={() => {
// Send data BACK to the previous screen
navigation.navigate('Cart', { addedItem: productId });
}}
/>
</View>
);
}
// --- UPDATING PARAMS (useful for forms passing back data) ---
navigation.setParams({ price: 69.99 }); // Updates the current screen's params
// --- DEFAULT PARAMS ---
// Set fallback values in initialParams to avoid undefined errors:
<Stack.Screen
name="ProductDetails"
component={ProductDetailsScreen}
initialParams={{ price: 0, productName: 'Unknown Product' }}
/>
Using useNavigation and useRoute Hooks
If you need navigation or route access deep inside a component tree (not at the screen level), use the hooks instead of prop drilling.
import { useNavigation, useRoute } from '@react-navigation/native';
// Works in ANY component inside NavigationContainer — no prop drilling
export default function ProductCard({ product }) {
const navigation = useNavigation();
const route = useRoute(); // Current screen's route info
return (
<TouchableOpacity
onPress={() => navigation.navigate('ProductDetails', { productId: product.id })}
>
<Text>{product.name}</Text>
</TouchableOpacity>
);
}
// Listen for focus / blur events (e.g., refresh data when returning to a screen)
import { useFocusEffect } from '@react-navigation/native';
import { useCallback } from 'react';
export default function HomeScreen() {
useFocusEffect(
useCallback(() => {
fetchLatestData(); // Runs every time this screen comes into focus
return () => {
// Optional cleanup when screen blurs
};
}, [])
);
}
Best Practices and Common Patterns
After working with React Navigation across many production apps, here are the patterns that consistently improve code quality and user experience.
Navigation Patterns Quick Reference
| Pattern | Use Case | Navigator |
|---|---|---|
| Auth Flow Guard | Show login/register before main app | Conditional Stack render |
| Modal Screen | Image viewer, confirmation dialogs | Stack with presentation: 'modal' |
| Tab + Stack | Most social/e-commerce apps | BottomTabs wrapping Stacks |
| Drawer + Stack | Admin panels, content-heavy apps | Drawer wrapping Stacks |
| Deep Linking | Push notifications, URLs to screens | NavigationContainer linking prop |
| Splash / Onboarding | First-launch flow | Stack with animationTypeForReplace |
Authentication Flow Pattern
The safest way to implement auth gating is to conditionally render different navigators based on auth state — React Navigation will handle the transition automatically.
import { NavigationContainer } from '@react-navigation/native';
import { useAuth } from './hooks/useAuth'; // Your auth context
import AuthStack from './navigation/AuthStack'; // Login, Register, ForgotPassword
import MainTabs from './navigation/MainTabs'; // Authenticated app
import SplashScreen from './screens/SplashScreen';
export default function App() {
const { user, isLoading } = useAuth();
if (isLoading) {
return <SplashScreen />;
}
return (
<NavigationContainer>
{/* React Navigation will animate between these on auth state change */}
{user ? <MainTabs /> : <AuthStack />}
</NavigationContainer>
);
}
TypeScript Support
React Navigation 6 has first-class TypeScript support. Defining a route param list catches type errors before they hit production.
import { NativeStackScreenProps } from '@react-navigation/native-stack';
// Define the param list for each screen
export type RootStackParamList = {
Home: undefined; // No params
Details: { itemId: number; title: string }; // Required params
Profile: { userId?: string }; // Optional param
Modal: undefined;
};
// Typed screen props — use this instead of any
export type DetailsScreenProps = NativeStackScreenProps<
RootStackParamList,
'Details'
>;
// In your screen component:
export default function DetailsScreen({ route, navigation }: DetailsScreenProps) {
// route.params.itemId is correctly typed as number
const { itemId, title } = route.params;
// ...
}
// Register the param list with the navigator for global type safety:
declare global {
namespace ReactNavigation {
interface RootParamList extends RootStackParamList {}
}
}
Key Takeaways
- Use
NavigationContaineronce at the root — never nest multiple containers. - Prefer native stack (
@react-navigation/native-stack) over the JS stack for better performance and correct native behaviour. - Each tab maintains its own stack state — switching tabs preserves navigation history within each tab.
- Auth gating via conditional rendering is simpler and safer than trying to navigate programmatically on login/logout.
- Use
useFocusEffectinstead ofuseEffectwhen you need to run code every time a screen becomes visible (e.g., data refresh). - Define a TypeScript param list early — it costs little to set up and saves significant debugging time as your app grows.
- Keep params serialisable — pass IDs and primitives, not functions or class instances, to support deep linking and state persistence.
"Good navigation is invisible. When users can always find what they need without thinking about how to get there, you've done your job."
— React Navigation Team
React Navigation's composable architecture means there's no single "correct" structure — the right pattern depends on your app's content hierarchy and how users think about moving through it. Start with the simplest structure that meets your needs (often a Tab + Stack combo), and add complexity like drawers or nested navigators only when the UX genuinely calls for it. With the patterns in this guide, you now have everything you need to build navigation that feels natural and performs like a first-class native app.