React Navigation Patterns
React Navigation patterns for stack, tab, drawer, and nested navigators in React Native
You are an expert in React Navigation for building cross-platform mobile apps with React Native. ## Key Points - Always define a `ParamList` type for every navigator and use it consistently for type-safe navigation. - Use `NativeStackNavigator` over `StackNavigator` for better performance — it uses native platform navigation primitives. - Keep navigation logic out of deeply nested components; pass callbacks or use `useNavigation` with proper typing. - Use `presentation: "modal"` or `presentation: "transparentModal"` on stack screens instead of building custom modal components. - Set `headerShown: false` on navigators that are nested inside other navigators to avoid double headers. - Use `useFocusEffect` instead of `useEffect` for data fetching that should re-run when navigating back to a screen. - **Navigating before the navigator is mounted**: Wrap early navigation calls (e.g., from deep links or notifications) in `navigationRef.isReady()` checks. - **Duplicate headers in nested navigators**: When a tab navigator is inside a stack, both show headers by default. Disable the inner one. - **Passing non-serializable params**: Navigation params must be serializable (no functions, class instances). Use global state or context for complex data. - **Forgetting to reset the stack on logout**: Use `CommonActions.reset` to clear the navigation history when switching between auth and main flows. - **Not handling the Android back button**: Stack navigators handle it automatically, but custom navigators or modals may need `BackHandler` listeners.
skilldb get react-native-skills/React Navigation PatternsFull skill: 281 linesReact Navigation Patterns — React Native
You are an expert in React Navigation for building cross-platform mobile apps with React Native.
Core Philosophy
Navigation in a mobile app is not just routing -- it is state management for which screen the user sees, what data each screen has, and how screens transition between each other. React Navigation models this as a tree of navigators, each maintaining its own state stack. The mental model is important: a stack navigator pushes and pops screens, a tab navigator switches between persistent branches, and these can be nested to create complex flows. Understanding this tree structure is the key to building navigation that feels natural on mobile.
Type safety is not optional for navigation. React Navigation's TypeScript support lets you define a ParamList type for every navigator, which ensures that navigation.navigate("Profile", { userId: "42" }) is verified at compile time -- both the route name and the parameter types. Without this, navigation bugs (wrong route name, missing parameter, wrong parameter type) only appear at runtime, often in production. The upfront cost of defining types is repaid every time the compiler catches a navigation bug that would otherwise be a crash report.
Screen components should not know how they were navigated to. A ProfileScreen should accept its data through typed route params and report user actions through callbacks or navigation events, not through direct calls to navigation.push. This separation means the same screen works in a stack, a tab, a modal, or a deep link context. When a screen directly manipulates the navigation state, it becomes coupled to a specific navigator structure and cannot be reused.
Anti-Patterns
-
Passing non-serializable objects as navigation params: Navigation params must be serializable because they are persisted for state restoration and deep linking. Passing functions, class instances, or React components as params will cause state restoration to fail and deep links to crash. Use IDs and look up the full objects at the destination.
-
Creating duplicate headers in nested navigators: When a tab navigator is nested inside a stack navigator, both default to showing a header. The result is a double header -- one from the stack, one from the tab. Set
headerShown: falseon the inner navigator to prevent this. -
Not resetting the navigation stack on logout: When the user logs out, the auth screens must replace the entire navigation stack, not just push on top. Without
CommonActions.reset, pressing the back button returns the user to authenticated screens. Use conditional rendering ofStack.Groupbased on auth state for clean separation. -
Using useEffect for data fetching in screens instead of useFocusEffect:
useEffectruns once when the component mounts but does not re-run when the user navigates back to the screen.useFocusEffectruns every time the screen gains focus, ensuring the user sees fresh data after returning from another screen. -
Navigating before the navigator is mounted: Deep links and push notifications that trigger navigation before
NavigationContainerhas finished mounting will silently fail. Guard early navigation calls withnavigationRef.isReady()or queue them until the navigator is ready.
Overview
React Navigation is the standard routing library for React Native. It provides stack, tab, drawer, and custom navigators that manage screen transitions, deep linking, and navigation state. Version 7 introduced a static API alongside the dynamic API, and Expo Router builds on top of React Navigation with file-based routing.
Core Concepts
Navigator Types
| Navigator | Use Case |
|---|---|
NativeStackNavigator | Primary screen-to-screen navigation with native transitions |
BottomTabNavigator | Bottom tab bar for top-level sections |
MaterialTopTabNavigator | Swipeable top tabs |
DrawerNavigator | Side menu navigation |
Basic Stack Setup
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
type RootStackParamList = {
Home: undefined;
Profile: { userId: string };
Settings: undefined;
};
const Stack = createNativeStackNavigator<RootStackParamList>();
function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
<Stack.Screen
name="Settings"
component={SettingsScreen}
options={{ presentation: "modal" }}
/>
</Stack.Navigator>
</NavigationContainer>
);
}
Type-Safe Navigation
import { NativeStackScreenProps } from "@react-navigation/native-stack";
type ProfileProps = NativeStackScreenProps<RootStackParamList, "Profile">;
function ProfileScreen({ route, navigation }: ProfileProps) {
const { userId } = route.params;
return (
<View>
<Text>User: {userId}</Text>
<Button
title="Go to Settings"
onPress={() => navigation.navigate("Settings")}
/>
</View>
);
}
useNavigation with Type Safety
import { useNavigation } from "@react-navigation/native";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
type NavigationProp = NativeStackNavigationProp<RootStackParamList>;
function SomeComponent() {
const navigation = useNavigation<NavigationProp>();
const goToProfile = (userId: string) => {
navigation.navigate("Profile", { userId });
};
}
Implementation Patterns
Nested Navigators (Tabs Inside Stack)
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { Ionicons } from "@expo/vector-icons";
type TabParamList = {
HomeTab: undefined;
SearchTab: undefined;
ProfileTab: undefined;
};
const Tab = createBottomTabNavigator<TabParamList>();
function MainTabs() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ focused, color, size }) => {
const icons: Record<keyof TabParamList, string> = {
HomeTab: focused ? "home" : "home-outline",
SearchTab: focused ? "search" : "search-outline",
ProfileTab: focused ? "person" : "person-outline",
};
return <Ionicons name={icons[route.name]} size={size} color={color} />;
},
tabBarActiveTintColor: "#6366f1",
headerShown: false,
})}
>
<Tab.Screen name="HomeTab" component={HomeScreen} />
<Tab.Screen name="SearchTab" component={SearchScreen} />
<Tab.Screen name="ProfileTab" component={ProfileScreen} />
</Tab.Navigator>
);
}
// Root stack wraps tabs
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Main"
component={MainTabs}
options={{ headerShown: false }}
/>
<Stack.Screen name="Detail" component={DetailScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
Authentication Flow
function App() {
const { isLoggedIn } = useAuth();
return (
<NavigationContainer>
<Stack.Navigator screenOptions={{ headerShown: false }}>
{isLoggedIn ? (
<Stack.Screen name="Main" component={MainTabs} />
) : (
<Stack.Group>
<Stack.Screen name="Login" component={LoginScreen} />
<Stack.Screen name="Register" component={RegisterScreen} />
</Stack.Group>
)}
</Stack.Navigator>
</NavigationContainer>
);
}
Deep Linking Configuration
const linking = {
prefixes: ["myapp://", "https://myapp.com"],
config: {
screens: {
Main: {
screens: {
HomeTab: "home",
ProfileTab: "profile",
},
},
Detail: "detail/:id",
Settings: "settings",
},
},
};
function App() {
return (
<NavigationContainer linking={linking} fallback={<LoadingScreen />}>
{/* navigators */}
</NavigationContainer>
);
}
Custom Header
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={({ route }) => ({
headerTitle: () => (
<Text style={{ fontWeight: "700", fontSize: 18 }}>
{route.params.userId}
</Text>
),
headerRight: () => (
<Pressable onPress={() => Alert.alert("Menu")}>
<Ionicons name="ellipsis-horizontal" size={24} />
</Pressable>
),
headerBackTitleVisible: false,
})}
/>
Screen Listeners and Focus Effects
import { useFocusEffect } from "@react-navigation/native";
import { useCallback } from "react";
function HomeScreen() {
useFocusEffect(
useCallback(() => {
// Runs when screen gains focus
fetchLatestData();
return () => {
// Cleanup when screen loses focus
cancelPendingRequests();
};
}, [])
);
}
Best Practices
- Always define a
ParamListtype for every navigator and use it consistently for type-safe navigation. - Use
NativeStackNavigatoroverStackNavigatorfor better performance — it uses native platform navigation primitives. - Keep navigation logic out of deeply nested components; pass callbacks or use
useNavigationwith proper typing. - Use
presentation: "modal"orpresentation: "transparentModal"on stack screens instead of building custom modal components. - Set
headerShown: falseon navigators that are nested inside other navigators to avoid double headers. - Use
useFocusEffectinstead ofuseEffectfor data fetching that should re-run when navigating back to a screen.
Common Pitfalls
- Navigating before the navigator is mounted: Wrap early navigation calls (e.g., from deep links or notifications) in
navigationRef.isReady()checks. - Duplicate headers in nested navigators: When a tab navigator is inside a stack, both show headers by default. Disable the inner one.
- Passing non-serializable params: Navigation params must be serializable (no functions, class instances). Use global state or context for complex data.
- Forgetting to reset the stack on logout: Use
CommonActions.resetto clear the navigation history when switching between auth and main flows. - Not handling the Android back button: Stack navigators handle it automatically, but custom navigators or modals may need
BackHandlerlisteners.
Install this skill directly: skilldb add react-native-skills
Related Skills
Reanimated Animations
High-performance animations in React Native using Reanimated and Gesture Handler
Eas Build Ota Updates
Deploying React Native apps with EAS Build, app store submission, and OTA updates via EAS Update
Expo Managed Workflow
Expo managed workflow for rapid React Native development with minimal native configuration
Native Modules Turbo Modules
Creating native modules and Turbo Modules to bridge platform-specific functionality into React Native
Offline Storage
Offline storage strategies in React Native using AsyncStorage, MMKV, and WatermelonDB
State Management Zustand Jotai
State management in React Native using Zustand and Jotai for scalable, performant app state