Skip to main content
Technology & EngineeringReact Native281 lines

React Navigation Patterns

React Navigation patterns for stack, tab, drawer, and nested navigators in React Native

Quick Summary17 lines
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 lines
Paste into your CLAUDE.md or agent config

React 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: false on 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 of Stack.Group based on auth state for clean separation.

  • Using useEffect for data fetching in screens instead of useFocusEffect: useEffect runs once when the component mounts but does not re-run when the user navigates back to the screen. useFocusEffect runs 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 NavigationContainer has finished mounting will silently fail. Guard early navigation calls with navigationRef.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

NavigatorUse Case
NativeStackNavigatorPrimary screen-to-screen navigation with native transitions
BottomTabNavigatorBottom tab bar for top-level sections
MaterialTopTabNavigatorSwipeable top tabs
DrawerNavigatorSide 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 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.

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.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.

Install this skill directly: skilldb add react-native-skills

Get CLI access →