Introduction
Navigation is the backbone of any mobile application. Users expect smooth, intuitive transitions between screens — swipe gestures to go back, bottom tabs for primary sections, and drawer menus for secondary navigation. React Navigation v5 delivers all of this through a completely rewritten, hook-based API that replaces the static configuration of v4 with a composable, component-driven architecture.
React Navigation v5 is the most widely adopted navigation library in the React Native ecosystem, powering navigation in millions of applications. Its component-based API feels natural to React developers, its TypeScript support is excellent, and its pluggable architecture allows deep customization of every aspect of the navigation experience. Whether you're building a simple two-screen app or a complex application with nested navigators, authentication flows, and deep linking, React Navigation v5 provides the building blocks you need.
Understanding React Navigation v5: Core Concepts
The Navigator Component Model
The fundamental shift in v5 is that navigators are React components, not configuration objects. Each navigator type (Stack, Tab, Drawer) is a component that accepts Screen children:
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
const Stack = createStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}The Navigation Container
NavigationContainer is the root component that manages the navigation tree and handles deep linking. Every navigator in your app must be nested inside a single NavigationContainer:
import { NavigationContainer, DefaultTheme, DarkTheme } from '@react-navigation/native';
import { useColorScheme } from 'react-native';
export default function App() {
const scheme = useColorScheme();
return (
<NavigationContainer theme={scheme === 'dark' ? DarkTheme : DefaultTheme}>
<RootNavigator />
</NavigationContainer>
);
}Hooks-Based API
v5 introduces hooks for all navigation operations, replacing the this.props.navigation pattern:
import { useNavigation, useRoute, useFocusEffect } from '@react-navigation/native';
function ProductScreen() {
const navigation = useNavigation();
const route = useRoute();
const { productId } = route.params;
useFocusEffect(
useCallback(() => {
// Screen is focused — load fresh data
fetchProduct(productId);
return () => {
// Screen loses focus — cleanup
cancelPendingRequests();
};
}, [productId])
);
return (
<View>
<Text>Product #{productId}</Text>
<Button
title="View Reviews"
onPress={() => navigation.navigate('Reviews', { productId })}
/>
</View>
);
}Architecture and Navigator Types
Stack Navigator
The stack navigator manages a stack of screens where each new screen is pushed on top and the back button pops it off. This is the most common navigation pattern for detail flows:
import { createStackNavigator } from '@react-navigation/stack';
type RootStackParamList = {
Home: undefined;
ProductDetail: { id: string; title: string };
Checkout: { cartItems: CartItem[] };
};
const Stack = createStackNavigator<RootStackParamList>();
function RootNavigator() {
return (
<Stack.Navigator
screenOptions={{
headerStyle: { backgroundColor: '#3b82f6' },
headerTintColor: '#fff',
headerTitleStyle: { fontWeight: 'bold' },
gestureEnabled: true,
cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS,
}}
>
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: 'Products' }}
/>
<Stack.Screen
name="ProductDetail"
component={ProductDetailScreen}
options={({ route }) => ({
title: route.params.title,
headerRight: () => (
<ShareButton productId={route.params.id} />
),
})}
/>
<Stack.Screen
name="Checkout"
component={CheckoutScreen}
options={{
gestureEnabled: false, // Prevent swipe back during checkout
headerLeft: () => null,
}}
/>
</Stack.Navigator>
);
}Bottom Tab Navigator
Tabs provide primary navigation between top-level sections of your app:
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
const Tab = createBottomTabNavigator();
function MainTabNavigator() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
let iconName: keyof typeof Ionicons.glyphMap;
switch (route.name) {
case 'Home':
iconName = focused ? 'home' : 'home-outline';
break;
case 'Search':
iconName = focused ? 'search' : 'search-outline';
break;
case 'Cart':
iconName = focused ? 'cart' : 'cart-outline';
break;
case 'Profile':
iconName = focused ? 'person' : 'person-outline';
break;
}
return <Ionicons name={iconName} size={size} color={color} />;
},
tabBarActiveTintColor: '#3b82f6',
tabBarInactiveTintColor: '#6b7280',
tabBarStyle: {
paddingBottom: 8,
paddingTop: 8,
height: 60,
},
})}
>
<Tab.Screen name="Home" component={HomeStack} />
<Tab.Screen name="Search" component={SearchStack} />
<Tab.Screen name="Cart" component={CartStack} options={{ tabBarBadge: 3 }} />
<Tab.Screen name="Profile" component={ProfileStack} />
</Tab.Navigator>
);
}Drawer Navigator
The drawer provides a side menu for secondary navigation:
import { createDrawerNavigator, DrawerContentScrollView, DrawerItem } from '@react-navigation/drawer';
const Drawer = createDrawerNavigator();
function CustomDrawerContent(props: any) {
const { signOut } = useAuth();
return (
<DrawerContentScrollView {...props}>
<DrawerItem
label="Home"
icon={({ color, size }) => (
<Ionicons name="home-outline" size={size} color={color} />
)}
onPress={() => props.navigation.navigate('Main')}
/>
<DrawerItem
label="Settings"
icon={({ color, size }) => (
<Ionicons name="settings-outline" size={size} color={color} />
)}
onPress={() => props.navigation.navigate('Settings')}
/>
<DrawerItem
label="Sign Out"
icon={({ color, size }) => (
<Ionicons name="log-out-outline" size={size} color={color} />
)}
onPress={signOut}
/>
</DrawerContentScrollView>
);
}
function AppDrawer() {
return (
<Drawer.Navigator drawerContent={(props) => <CustomDrawerContent {...props} />}>
<Drawer.Screen name="Main" component={MainTabNavigator} />
<Drawer.Screen name="Settings" component={SettingsScreen} />
</Drawer.Navigator>
);
}Step-by-Step Implementation
Installation
# Core package
npm install @react-navigation/native
# Dependencies
npm install react-native-screens react-native-safe-area-context
# Navigator types (install what you need)
npm install @react-navigation/stack @react-navigation/bottom-tabs @react-navigation/drawer
# For gesture support
npm install react-native-gesture-handler
# For tab bar icons
npm install @expo/vector-iconsSetting Up the Root Structure
// App.tsx
import 'react-native-gesture-handler';
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import { AuthNavigator } from './navigators/AuthNavigator';
import { AppNavigator } from './navigators/AppNavigator';
import { LoadingScreen } from './screens/LoadingScreen';
function RootNavigation() {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) return <LoadingScreen />;
return (
<NavigationContainer>
{isAuthenticated ? <AppNavigator /> : <AuthNavigator />}
</NavigationContainer>
);
}
export default function App() {
return (
<AuthProvider>
<RootNavigation />
</AuthProvider>
);
}Type-Safe Navigation with TypeScript
Define your navigation types for compile-time safety:
// types/navigation.ts
import { NavigatorScreenParams } from '@react-navigation/native';
export type RootStackParamList = {
Auth: NavigatorScreenParams<AuthStackParamList>;
Main: NavigatorScreenParams<MainTabParamList>;
Modal: { title: string; content: string };
};
export type AuthStackParamList = {
Login: undefined;
Register: undefined;
ForgotPassword: { email?: string };
};
export type MainTabParamList = {
HomeStack: NavigatorScreenParams<HomeStackParamList>;
Search: { initialQuery?: string };
Profile: { userId?: string };
};
export type HomeStackParamList = {
ProductList: { category?: string };
ProductDetail: { id: string };
};Real-World Use Cases
Use Case 1: Authentication Flow with Conditional Navigation
function AppNavigator() {
const { isAuthenticated } = useAuth();
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
{isAuthenticated ? (
<>
<Stack.Screen name="Main" component={MainTabNavigator} />
<Stack.Screen
name="Modal"
component={ModalScreen}
options={{ presentation: 'modal' }}
/>
</>
) : (
<>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} />
</>
)}
</Stack.Navigator>
);
}Use Case 2: Deep Linking Configuration
const linking = {
prefixes: ['myapp://', 'https://myapp.com'],
config: {
screens: {
Main: {
screens: {
HomeStack: {
screens: {
ProductList: 'products',
ProductDetail: 'products/:id',
},
},
Search: 'search',
Profile: 'profile/:userId?',
},
},
Modal: 'modal/:title',
},
},
};
function App() {
return (
<NavigationContainer linking={linking}>
<AppNavigator />
</NavigationContainer>
);
}Use Case 3: Nested Stacks Within Tabs
Each tab typically maintains its own navigation stack so users can drill into detail screens without losing tab state:
const HomeStack = createStackNavigator<HomeStackParamList>();
function HomeStackNavigator() {
return (
<HomeStack.Navigator>
<HomeStack.Screen name="ProductList" component={ProductListScreen} />
<HomeStack.Screen name="ProductDetail" component={ProductDetailScreen} />
</HomeStack.Navigator>
);
}
// Then use HomeStackNavigator as a tab screen
<Tab.Screen name="Home" component={HomeStackNavigator} />Best Practices for Production
-
Define navigation types upfront: Create a comprehensive TypeScript type definition for all navigation params before building screens. This catches navigation errors at compile time.
-
Use
navigation.reset()for auth state changes: When logging out, reset the navigation state instead of navigating back, which could expose authenticated screens during the transition. -
Implement proper back behavior on Android: Use
BackHandlerAPI for custom back button handling, but ensure the default stack pop behavior works correctly in all nested navigator scenarios. -
Optimize header rendering: Use
optionsas a function of route params to compute header elements dynamically. Memoize expensive header components to prevent re-renders during transitions. -
Handle deep links gracefully: Test deep links with
npx uri-scheme open myapp://products/123 --androidand verify that parameters are correctly parsed and passed to screens. -
Use
unmountOnBlurselectively: By default, screens stay mounted when navigating away. SetunmountOnBlur: truefor screens that need fresh data on each visit (like search results). -
Implement proper loading states: Use
useFocusEffectto refetch data when a screen regains focus, showing loading indicators only when the data actually needs refreshing. -
Test navigation flows in isolation: Use
@react-navigation/test-rendererto test navigation transitions and screen configuration without running a full app.
Common Pitfalls and Solutions
| Pitfall | Impact | Solution |
|---|---|---|
Multiple NavigationContainer instances | Navigation state conflicts and broken deep links | Ensure only one NavigationContainer wraps your entire app |
| Passing non-serializable params (functions, class instances) | State persistence and deep linking break silently | Use route params only for serializable data; pass callbacks via context |
| Tab navigator inside stack loses state on back | User's scroll position and input state lost | Keep tab navigator as the root; nest stacks inside each tab |
| Gesture handler not initialized | Swipe-back gesture crashes the app | Import react-native-gesture-handler at the top of your entry file |
| Deep link params not reaching nested screen | Deep link navigates to wrong screen | Verify the linking config matches your navigator hierarchy exactly |
| Header button re-rendering every frame | Performance jank during transitions | Memoize header components with React.memo and stable callbacks |
Performance Optimization
Preventing Unnecessary Re-renders
// Memoize screen components
const ProductDetailScreen = React.memo(function ProductDetail() {
const route = useRoute<RouteProp<RootStackParamList, 'ProductDetail'>>();
// Component only re-renders when route params change
return <ProductDetailContent id={route.params.id} />;
});
// Use stable callbacks for navigation
function useStableNavigate() {
const navigation = useNavigation();
return useCallback(
(screen: string, params?: object) => navigation.navigate(screen, params),
[navigation]
);
}Lazy Loading Screens
<Tab.Navigator
lazy={true} // Only render tab screens when they're first visited
tabBarOptions={{
lazy: true,
}}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Search" component={SearchScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>Comparison with Alternatives
| Feature | React Navigation v5 | React Native Navigation (Wix) | Expo Router |
|---|---|---|---|
| Architecture | JS-based navigation | Truly native navigation controllers | File-system routing on React Navigation |
| Performance | Good (bridge-based) | Excellent (native) | Good (JS-based) |
| Customization | Highly customizable | Limited to native capabilities | File-system conventions |
| Deep Linking | Manual configuration | Manual configuration | Automatic |
| TypeScript | Strong type support | Good type support | Built-in typed routes |
| Community | Largest ecosystem | Smaller community | Growing rapidly |
| Learning Curve | Medium | High | Low |
| Web Support | Via React Native Web | Not supported | First-class |
Testing Strategies
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
function renderWithNavigation(component: React.ReactElement) {
const Stack = createStackNavigator();
return render(
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Test" component={() => component} />
</Stack.Navigator>
</NavigationContainer>
);
}
describe('Product Navigation', () => {
it('should navigate to detail screen when product is tapped', async () => {
const { getByText, findByText } = renderWithNavigation(
<ProductList products={mockProducts} />
);
fireEvent.press(getByText('Running Shoes'));
const detailTitle = await findByText('Running Shoes Details');
expect(detailTitle).toBeTruthy();
});
});Deep Linking and Universal Links
Configuring Deep Links
Deep linking allows users to navigate directly to specific screens via URLs. React Navigation v5 supports deep linking through the linking configuration:
const linking = {
prefixes: ['myapp://', 'https://myapp.com'],
config: {
screens: {
Main: {
screens: {
Home: {
screens: {
ProductList: 'products',
ProductDetail: 'products/:id',
},
},
Search: 'search/:query?',
Profile: 'profile/:userId',
},
},
Auth: {
screens: {
Login: 'login',
Register: 'register',
ForgotPassword: 'forgot-password',
},
},
Modal: 'modal/:title',
},
},
// Custom function to parse deep link parameters
async getInitialURL() {
// Check if app was opened from a deep link
const url = await Linking.getInitialURL();
if (url != null) return url;
// Check for push notification deep links
const message = await messaging().getInitialNotification();
return message?.data?.deepLink;
},
// Subscribe to incoming deep links
subscribe(listener) {
const linkingSubscription = Linking.addEventListener('url', ({ url }) => {
listener(url);
});
const unsubscribeNotification = messaging().onNotificationOpenedApp((message) => {
const url = message?.data?.deepLink;
if (url) listener(url);
});
return () => {
linkingSubscription.remove();
unsubscribeNotification();
};
},
};Universal Links (iOS) and App Links (Android)
Configure universal links for seamless web-to-app navigation:
// ios/myapp/AssociatedDomains.entitlements
// <key>com.apple.developer.associated-domains</key>
// <array>
// <string>applinks:myapp.com</string>
// </array>
// android/app/src/main/AndroidManifest.xml
// <intent-filter android:autoVerify="true">
// <action android:name="android.intent.action.VIEW" />
// <category android:name="android.intent.category.DEFAULT" />
// <category android:name="android.intent.category.BROWSABLE" />
// <data android:scheme="https" android:host="myapp.com" />
// </intent-filter>
// Testing deep links in development
// iOS: xcrun simctl openurl booted "myapp://products/123"
// Android: adb shell am start -a android.intent.action.VIEW -d "myapp://products/123"Navigation State Persistence
Persist navigation state so users return to where they left off:
import AsyncStorage from '@react-native-async-storage/async-storage';
const PERSISTENCE_KEY = 'NAVIGATION_STATE';
function App() {
const [isReady, setIsReady] = useState(false);
const [initialState, setInitialState] = useState();
useEffect(() => {
const restoreState = async () => {
try {
const savedState = await AsyncStorage.getItem(PERSISTENCE_KEY);
if (savedState) {
setInitialState(JSON.parse(savedState));
}
} finally {
setIsReady(true);
}
};
if (!isReady) {
restoreState();
}
}, [isReady]);
if (!isReady) return <LoadingScreen />;
return (
<NavigationContainer
initialState={initialState}
onStateChange={(state) => {
AsyncStorage.setItem(PERSISTENCE_KEY, JSON.stringify(state));
}}
linking={linking}
>
<AppNavigator />
</NavigationContainer>
);
}Conditional Navigation Based on Auth State
Implement authentication-aware navigation that redirects unauthenticated users:
function useAuthNavigation() {
const { isAuthenticated, isLoading } = useAuth();
const navigation = useNavigation();
useEffect(() => {
if (isLoading) return;
if (!isAuthenticated) {
// Reset to auth stack
navigation.reset({
index: 0,
routes: [{ name: 'Auth' }],
});
}
}, [isAuthenticated, isLoading, navigation]);
return { isAuthenticated, isLoading };
}
// In your root navigator
function AppNavigator() {
const { isAuthenticated } = useAuth();
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
{isAuthenticated ? (
<Stack.Screen name="Main" component={MainTabNavigator} />
) : (
<Stack.Screen name="Auth" component={AuthNavigator} />
)}
</Stack.Navigator>
);
}Navigation Events and Lifecycle Hooks
React Navigation v5 provides lifecycle hooks for screen-level data loading:
import { useFocusEffect, useIsFocused } from '@react-navigation/native';
function ProductListScreen() {
const [products, setProducts] = useState([]);
const isFocused = useIsFocused();
// Refetch data when screen gains focus
useFocusEffect(
useCallback(() => {
let isActive = true;
fetchProducts().then((data) => {
if (isActive) {
setProducts(data);
}
});
return () => {
isActive = false; // Cleanup if screen loses focus before fetch completes
};
}, [])
);
// Use isFocused for conditional rendering
if (!isFocused) return null;
return <ProductList data={products} />;
}Performance Optimization
Memory Management for Large Navigation Stacks
// Unmount screens that are far from the current screen
<Stack.Navigator
detachInactiveScreens={true}
screenOptions={{
freezeOnBlur: true, // Freeze inactive screens to save memory
}}
>
<Stack.Screen name="ProductList" component={ProductListScreen} />
<Stack.Screen name="ProductDetail" component={ProductDetailScreen} />
</Stack.Navigator>Avoiding Unnecessary Re-renders
// Memoize screen components to prevent re-renders during navigation transitions
const ProductScreen = React.memo(function ProductScreen() {
const route = useRoute();
const { productId } = route.params;
return <ProductContent id={productId} />;
});
// Use stable references for navigation params
function useStableParams<T>(params: T): T {
const ref = useRef(params);
if (JSON.stringify(ref.current) !== JSON.stringify(params)) {
ref.current = params;
}
return ref.current;
}Deep Linking Patterns for Real Apps
Deep linking is critical for user acquisition, retention, and push notification handling. React Navigation's linking configuration maps URLs to screens, but real-world patterns require handling authentication state, nested navigators, and URL parameters.
Auth-Aware Deep Linking
Most apps need to redirect unauthenticated users to login when they open a deep link. React Navigation handles this elegantly with conditional navigation:
const linking = {
prefixes: ["myapp://", "https://myapp.com"],
config: {
screens: {
Auth: {
screens: {
Login: "login",
Register: "register",
},
},
Main: {
screens: {
Home: "home",
Product: {
path: "product/:id",
parse: {
id: (id: string) => id,
},
},
Order: {
path: "order/:orderId",
parse: {
orderId: (id: string) => id,
},
},
},
},
},
},
async getInitialURL() {
return await Linking.getInitialURL();
},
subscribe(listener: (url: string) => void) {
const sub = Linking.addEventListener("url", ({ url }) => listener(url));
return () => sub.remove();
},
};Push Notification Navigation
When users tap a push notification, navigate to a specific screen by storing the pending target and applying it once the app is ready:
import { createNavigationContainerRef } from "@react-navigation/native";
export const navigationRef = createNavigationContainerRef();
let pendingNavigation: { screen: string; params?: Record<string, any> } | null = null;
export function handleNotificationOpen(notification: any) {
const { screen, params } = notification.data;
if (navigationRef.isReady()) {
navigationRef.navigate(screen as never, params as never);
} else {
pendingNavigation = { screen, params };
}
}
export function applyPendingNavigation() {
if (pendingNavigation && navigationRef.isReady()) {
navigationRef.navigate(
pendingNavigation.screen as never,
pendingNavigation.params as never
);
pendingNavigation = null;
}
}Shared Element Transitions
For polished transitions between screens (like tapping a product image to see full details), React Navigation integrates with react-native-shared-element:
import { SharedElement } from "react-navigation-shared-element";
function ProductCard({ product }) {
return (
<SharedElement id={`product.${product.id}.image`}>
<Image source={{ uri: product.image }} style={styles.thumbnail} />
</SharedElement>
);
}Testing Navigation Flows
Testing navigation prevents regressions in user flows:
import { render, fireEvent, waitFor } from "@testing-library/react-native";
import { NavigationContainer } from "@react-navigation/native";
function renderWithNavigation(component: React.ReactElement) {
const Stack = createNativeStackNavigator();
return render(
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Test" component={() => component} />
</Stack.Navigator>
</NavigationContainer>
);
}
describe("Product Flow", () => {
it("navigates to product detail on tap", async () => {
const { getByTestId, getByText } = renderWithNavigation(
<ProductList products={mockProducts} />
);
fireEvent.press(getByTestId("product-1"));
await waitFor(() => {
expect(getByText("Product Details")).toBeTruthy();
});
});
});Future Outlook
React Navigation continues to evolve with React Native's new architecture. The v7 preview (already available) brings first-class support for Fabric, improved TypeScript inference, static API configuration alongside the component API, and deeper integration with Expo Router for file-system based routing. The library remains the de facto standard for React Native navigation, with virtually every React Native app depending on it.
Conclusion
React Navigation v5 transformed React Native navigation from a configuration-heavy chore into a composable, hook-based experience that feels natural to React developers. Its component-based API, TypeScript support, and extensive customization options make it the clear choice for navigation in React Native applications.
Key takeaways:
- Navigators are React components — composable, configurable, and familiar
- Hooks replace class-based navigation —
useNavigation,useRoute,useFocusEffect - TypeScript types catch navigation errors at compile time — define params upfront
- Nesting navigators creates complex flows — stacks inside tabs inside drawers
- Deep linking is built-in — configure once, works on iOS and Android
- Performance is tunable — lazy loading, memoization, and selective unmounting
- The ecosystem is massive — community libraries, templates, and guides everywhere
Install React Navigation v5 today with npm install @react-navigation/native react-native-screens react-native-safe-area-context and build fluid, native-feeling navigation for your React Native app.