Claude Skill

mobile-navigation-react-navigation

React Navigation 7+ patterns - static and dynamic APIs, type-safe navigation, stack/tab/drawer navigators, deep linking, authentication flows, screen preloading, header customization

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_mobile-navigation-react-navigation_skills_mobile-navigation-react-navigation-3a51ef5.zip · 18 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-navigation-react-navigation/skills/mobile-navigation-react-navigation
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

React Navigation Patterns

Quick Guide: Use the static API for simpler TypeScript inference and automatic deep linking config. Use the dynamic API when you need runtime-dynamic screen lists. Always declare a global RootParamList for type-safe useNavigation everywhere. Use createNativeStackNavigator (not the JS stack) for production performance. Auth flows use conditional screen rendering via the if callback (static) or conditional JSX (dynamic). Deep linking config lives per-screen in the static API -- no separate config object needed.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST declare a global ReactNavigation.RootParamList interface so useNavigation is type-safe without manual annotation)

(You MUST use createNativeStackNavigator for production apps -- the JS stack (@react-navigation/stack) is significantly slower and only needed for highly custom transitions)

(You MUST use popTo() to navigate back to a previous screen in the stack -- navigate() in v7 no longer pops back to existing screens)

(You MUST wrap useFocusEffect callbacks in useCallback -- without it, the effect runs on every render, not just focus changes)

(You MUST NOT use navigation.navigate('NestedScreen') to reach screens in child navigators -- v7 removed implicit nested navigation; use explicit parent targeting)

</critical_requirements>


Auto-detection: React Navigation, @react-navigation, createNativeStackNavigator, createBottomTabNavigator, createDrawerNavigator, createStaticNavigation, NavigationContainer, useNavigation, useRoute, useFocusEffect, usePreventRemove, StaticParamList, StaticScreenProps, NativeStackNavigationProp, CompositeNavigationProp, NavigatorScreenParams, deep linking, linking config, headerSearchBarOptions, headerLargeTitle, popTo, preload

When to use:

  • Setting up navigation structure (stack, tab, drawer) in a React Native app
  • Choosing between static API and dynamic API for navigator configuration
  • Adding type-safe navigation with TypeScript (param lists, typed hooks)
  • Configuring deep linking (URL prefixes, path params, universal links)
  • Implementing authentication flows with conditional screen rendering
  • Customizing headers (large titles, search bars, custom buttons)
  • Preloading screens for perceived performance
  • Preventing back navigation for unsaved changes

When NOT to use:

  • File-based routing with a managed workflow (uses its own router built on React Navigation)
  • Web-only React apps (use a web router)
  • Simple single-screen apps with no navigation

Key patterns covered:

  • Static API vs dynamic API: when to use each
  • Global RootParamList declaration for type-safe hooks everywhere
  • Native stack vs JS stack performance trade-offs
  • Auth flow with conditional screens (static if callback or dynamic JSX)
  • Deep linking configuration (per-screen in static, linking prop in dynamic)
  • Screen preloading with navigation.preload()
  • useFocusEffect for screen lifecycle management
  • usePreventRemove for unsaved changes guards
  • Header customization: large titles, search bars, form sheets

Detailed Resources:

  • examples/core.md - Static API setup, dynamic API setup, type-safe navigation, global RootParamList
  • examples/patterns.md - Auth flows, deep linking, modals, tab navigator with nested stacks
  • examples/advanced.md - Screen preloading, state persistence, usePreventRemove, useFocusEffect, header customization
  • reference.md - Decision frameworks, screen options cheat sheet, v6-to-v7 migration



<decision_framework>

Decision Framework

Static vs Dynamic API

Starting a new navigation setup?
|-- Can all screens be defined at build time?
|   |-- YES --> Static API (simpler TS, auto deep linking)
|   +-- NO  --> Dynamic API (runtime screen lists)
|
|-- Migrating incrementally from v6?
|   +-- YES --> Dynamic API at root, static for new navigators
|       (use getComponent() and createPathConfigForStaticNavigation)
|
|-- Need to wrap navigator with providers (e.g. context)?
|   +-- Use static API with .with() method

Navigator Type

What navigation pattern?
|-- Linear flow (onboarding, checkout) --> Stack Navigator
|-- Main app sections with persistent bar --> Bottom Tab Navigator
|-- Side menu / settings panel --> Drawer Navigator
|-- Modal overlays --> Stack with presentation: "modal"
|-- Bottom sheets --> Stack with presentation: "formSheet"
|-- Combination --> Nest navigators (tabs inside stack, stacks inside tabs)

Navigation Method

How to move between screens?
|-- Push new screen forward --> navigation.navigate("Screen", params)
|-- Go back to specific screen --> navigation.popTo("Screen", params)
|-- Go back one screen --> navigation.goBack()
|-- Replace current screen --> navigation.replace("Screen", params)
|-- Reset entire stack --> navigation.reset({ routes: [...] })
|-- Navigate to nested screen --> navigation.navigate("Parent", { screen: "Child" })

</decision_framework>


<red_flags>

RED FLAGS

High Priority Issues:

  • Using navigate() to go back to a previous screen -- v7 changed behavior; navigate() stays on current screen if target exists. Use popTo() instead.
  • Using navigation.navigate("NestedScreen") to reach child navigator screens -- removed in v7. Must use navigate("ParentScreen", { screen: "NestedScreen" }).
  • Using JS stack (@react-navigation/stack) for production without a specific need for custom transitions -- native stack is significantly more performant.
  • Missing global RootParamList declaration -- every useNavigation() call is untyped, losing the primary benefit of TypeScript with React Navigation.
  • Using a custom header function and expecting native features (large title, search bar, blur) -- custom headers disable all native header functionality.

Medium Priority Issues:

  • Inline component functions in <Stack.Screen component={() => <MyScreen />} /> -- creates a new component on every render, causing unmount/remount. Always pass a reference.
  • Not using useFocusEffect for screen-specific side effects -- useEffect runs even when the screen is covered by another screen in the stack.
  • Mutating navigation state directly (caught in dev mode in v7, silent corruption in prod).
  • Missing fonts property in custom theme -- required in v7, crashes without it.

Gotchas & Edge Cases:

  • useFocusEffect callback must be wrapped in useCallback -- without it, the effect fires on every render, not just focus changes
  • usePreventRemove only fires for navigation state removal (back, pop, reset) -- it does NOT fire when the screen is merely unfocused (push, tab switch)
  • Preloaded screens cannot dispatch navigation actions or call navigation.setOptions() until actually navigated to
  • Screen options can be an object or a function receiving { route, navigation } -- use the function form when options depend on route params
  • headerSearchBarOptions requires contentInsetAdjustmentBehavior="automatic" on your ScrollView/FlatList for proper layout
  • headerBackButtonDisplayMode replaced headerBackTitleVisible in v7 -- values are "default", "generic", or "minimal"
  • unmountOnBlur removed from tabs/drawer in v7 -- use popToTopOnBlur: true or the useIsFocused pattern instead
  • Navigation state frozen in dev mode -- if you were mutating state directly, you'll get runtime errors in v7 dev builds
  • Android requires RNScreensFragmentFactory setup in MainActivity -- without it, View state is lost during Activity restarts
  • The Link component changed from path-based to screen-based: <Link screen="Profile" params={{ userId }}> not <Link to="/profile/123">

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md

(You MUST declare a global ReactNavigation.RootParamList interface so useNavigation is type-safe without manual annotation)

(You MUST use createNativeStackNavigator for production apps -- the JS stack (@react-navigation/stack) is significantly slower and only needed for highly custom transitions)

(You MUST use popTo() to navigate back to a previous screen in the stack -- navigate() in v7 no longer pops back to existing screens)

(You MUST wrap useFocusEffect callbacks in useCallback -- without it, the effect runs on every render, not just focus changes)

(You MUST NOT use navigation.navigate('NestedScreen') to reach screens in child navigators -- v7 removed implicit nested navigation; use explicit parent targeting)

Failure to follow these rules will cause untyped navigation, performance issues, broken back navigation, and runtime errors.

</critical_reminders>

Files (skills)
  • examples
    • advanced.md 8.7 KB
      # React Navigation - Advanced Patterns
      
      > Screen preloading, state persistence, navigation guards, header customization. See [patterns.md](patterns.md) for auth flows and deep linking.
      
      ---
      
      ## Pattern 1: useFocusEffect for Resource Management
      
      Screens in a stack stay mounted when covered by another screen. Use `useFocusEffect` to start/stop work based on screen visibility.
      
      ```typescript
      import { useCallback, useState } from "react";
      import { useFocusEffect } from "@react-navigation/native";
      
      const POLL_INTERVAL_MS = 30_000;
      
      // WebSocket connection: connect on focus, disconnect on blur
      function ChatScreen({ roomId }: { roomId: string }) {
        useFocusEffect(
          useCallback(() => {
            const ws = new WebSocket(`wss://chat.example.com/rooms/${roomId}`);
      
            ws.onopen = () => {
              // Connected
            };
      
            // Cleanup: disconnect when screen loses focus
            return () => {
              ws.close();
            };
          }, [roomId])
        );
      
        return <ChatUI />;
      }
      
      // Polling: start on focus, stop on blur
      function NotificationsScreen() {
        const [notifications, setNotifications] = useState([]);
      
        useFocusEffect(
          useCallback(() => {
            const fetch = async () => {
              const data = await api.getNotifications();
              setNotifications(data);
            };
      
            fetch();
            const interval = setInterval(fetch, POLL_INTERVAL_MS);
      
            return () => clearInterval(interval);
          }, [])
        );
      
        return <NotificationList data={notifications} />;
      }
      ```
      
      **Critical:** The callback passed to `useFocusEffect` MUST be wrapped in `useCallback`. Without it, the effect registers a new callback on every render, causing setup/teardown on every render cycle instead of only on focus/blur.
      
      ---
      
      ## Pattern 2: usePreventRemove for Unsaved Changes
      
      Prevent the user from navigating away when there are unsaved changes.
      
      ```typescript
      import { usePreventRemove } from "@react-navigation/native";
      import { Alert } from "react-native";
      import { useNavigation } from "@react-navigation/native";
      
      function EditProfileScreen() {
        const navigation = useNavigation();
        const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
      
        usePreventRemove(hasUnsavedChanges, ({ data }) => {
          Alert.alert(
            "Discard changes?",
            "You have unsaved changes. Are you sure you want to leave?",
            [
              { text: "Stay", style: "cancel" },
              {
                text: "Discard",
                style: "destructive",
                onPress: () => {
                  // Dispatch the blocked action to proceed
                  navigation.dispatch(data.action);
                },
              },
            ]
          );
        });
      
        return (
          <TextInput
            onChangeText={() => setHasUnsavedChanges(true)}
            // ...
          />
        );
      }
      ```
      
      **Limitations:**
      
      - Only fires for removal actions (back, pop, reset) -- NOT for screen being covered (push, tab switch)
      - Better alternative for data preservation: auto-save to persistent storage and offer restore on return
      
      ---
      
      ## Pattern 3: Screen Preloading
      
      Preload heavy screens in the background before the user navigates. The screen renders off-screen with all hooks running.
      
      ```typescript
      import { useNavigation } from "@react-navigation/native";
      import { useCallback } from "react";
      
      function ProductList({ products }: { products: Product[] }) {
        const navigation = useNavigation();
      
        // Preload on long press -- data fetching starts before user taps
        const handleLongPress = useCallback(
          (productId: string) => {
            navigation.preload("ProductDetail", { productId });
          },
          [navigation]
        );
      
        const handlePress = useCallback(
          (productId: string) => {
            navigation.navigate("ProductDetail", { productId });
          },
          [navigation]
        );
      
        const renderItem = useCallback(
          ({ item }: { item: Product }) => (
            <ProductCard
              product={item}
              onPress={() => handlePress(item.id)}
              onLongPress={() => handleLongPress(item.id)}
            />
          ),
          [handlePress, handleLongPress]
        );
      
        return (
          <FlatList
            data={products}
            renderItem={renderItem}
            keyExtractor={(item) => item.id}
          />
        );
      }
      ```
      
      **Preloaded screen limitations:**
      
      - Cannot dispatch navigation actions
      - Cannot call `navigation.setOptions()`
      - Cannot listen to navigator events
      - These restrictions lift once the user actually navigates to the screen
      
      ---
      
      ## Pattern 4: Navigation State Persistence
      
      Restore the navigation state across app restarts (useful for development and optional for production).
      
      ```typescript
      import AsyncStorage from "@react-native-async-storage/async-storage";
      import {
        NavigationContainer,
        type NavigationState,
      } from "@react-navigation/native";
      import { useCallback, useEffect, useState } from "react";
      
      const NAV_STATE_KEY = "NAVIGATION_STATE_V7";
      
      export function App() {
        const [isReady, setIsReady] = useState(false);
        const [initialState, setInitialState] = useState<NavigationState | undefined>();
      
        useEffect(() => {
          const restore = async () => {
            try {
              const saved = await AsyncStorage.getItem(NAV_STATE_KEY);
              if (saved) {
                setInitialState(JSON.parse(saved));
              }
            } catch {
              // Ignore restore errors -- start fresh
            } finally {
              setIsReady(true);
            }
          };
      
          restore();
        }, []);
      
        const handleStateChange = useCallback((state: NavigationState | undefined) => {
          if (state) {
            AsyncStorage.setItem(NAV_STATE_KEY, JSON.stringify(state));
          }
        }, []);
      
        if (!isReady) return <SplashScreen />;
      
        return (
          <NavigationContainer
            initialState={initialState}
            onStateChange={handleStateChange}
          >
            <RootNavigator />
          </NavigationContainer>
        );
      }
      ```
      
      **Caveats:**
      
      - All params must be serializable (no functions, class instances, or circular references)
      - Consider clearing persisted state on app version updates or if the screen structure changes
      - If the app crashes on a specific screen, persisted state could cause a crash loop -- add error boundaries that clear state
      
      ---
      
      ## Pattern 5: Header Customization with Native Features
      
      Native stack supports platform-native header features. These only work when NOT using a custom `header` function.
      
      ```typescript
      // Large title (iOS) -- collapses on scroll
      <Stack.Screen
        name="Settings"
        component={SettingsScreen}
        options={{
          title: "Settings",
          headerLargeTitleEnabled: true,
          headerLargeStyle: { backgroundColor: "#F5F5F5" },
          headerLargeTitleStyle: { fontWeight: "bold" },
          headerLargeTitleShadowVisible: false,
        }}
      />
      ```
      
      ```typescript
      // Search bar in header (iOS + Android)
      function SettingsScreen() {
        const navigation = useNavigation();
        const [searchQuery, setSearchQuery] = useState("");
      
        // Must use useLayoutEffect to set options before first paint
        useLayoutEffect(() => {
          navigation.setOptions({
            headerSearchBarOptions: {
              placeholder: "Search settings...",
              onChangeText: (event: { nativeEvent: { text: string } }) => {
                setSearchQuery(event.nativeEvent.text);
              },
              hideWhenScrolling: true,
            },
          });
        }, [navigation]);
      
        return (
          // contentInsetAdjustmentBehavior required for proper search bar layout
          <ScrollView contentInsetAdjustmentBehavior="automatic">
            <SettingsContent filter={searchQuery} />
          </ScrollView>
        );
      }
      ```
      
      ```typescript
      // Custom header buttons (preserves native header features)
      <Stack.Screen
        name="Profile"
        component={ProfileScreen}
        options={{
          headerRight: () => (
            <Pressable onPress={handleEdit}>
              <Text>Edit</Text>
            </Pressable>
          ),
        }}
      />
      ```
      
      **Critical:** If you provide a custom `header` function (not `headerLeft`/`headerRight`), ALL native features are disabled: large titles, search bars, blur effects, native back button.
      
      ---
      
      ## Pattern 6: Form Sheet Presentation (iOS/Android)
      
      ```typescript
      <Stack.Screen
        name="Filter"
        component={FilterScreen}
        options={{
          presentation: "formSheet",
          sheetAllowedDetents: [0.25, 0.5, 1.0],
          sheetInitialDetentIndex: 1,
          sheetGrabberVisible: true,
          sheetCornerRadius: 16,
          sheetExpandsWhenScrolledToEdge: true,
        }}
      />
      ```
      
      **Detent values:** Fractions of screen height (0.25 = quarter screen) or `"fitToContents"` for auto-sizing.
      
      ---
      
      ## Pattern 7: Theme Configuration (v7 Requirement)
      
      v7 themes require a `fonts` property. Always spread `DefaultTheme` to include it.
      
      ```typescript
      import { DefaultTheme, type Theme } from "@react-navigation/native";
      
      const APP_THEME: Theme = {
        ...DefaultTheme,
        colors: {
          ...DefaultTheme.colors,
          primary: "#007AFF",
          background: "#FFFFFF",
          card: "#F5F5F5",
          text: "#1C1C1E",
          border: "#E5E5EA",
          notification: "#FF3B30",
        },
        // fonts is inherited from DefaultTheme spread -- DON'T omit it
      };
      
      // Static API
      <Navigation theme={APP_THEME} />
      
      // Dynamic API
      <NavigationContainer theme={APP_THEME}>
      ```
      
    • core.md 9.5 KB
      # React Navigation - Core Setup & Type Safety
      
      > Static and dynamic API setup, global type declarations, typed hooks. See [SKILL.md](../SKILL.md) for decision guidance and red flags.
      
      **Prerequisites**: React Navigation 7+ installed with `@react-navigation/native`, `@react-navigation/native-stack`, `react-native-screens`, `react-native-safe-area-context`.
      
      ---
      
      ## Pattern 1: Complete Static API Setup
      
      ```typescript
      // app.tsx
      import { createStaticNavigation } from "@react-navigation/native";
      import { createNativeStackNavigator } from "@react-navigation/native-stack";
      import type { StaticParamList, StaticScreenProps } from "@react-navigation/native";
      import { HomeScreen } from "./screens/home-screen";
      import { ProfileScreen } from "./screens/profile-screen";
      import { SettingsScreen } from "./screens/settings-screen";
      
      const RootStack = createNativeStackNavigator({
        initialRouteName: "Home",
        screenOptions: {
          headerTintColor: "#007AFF",
          headerStyle: { backgroundColor: "#FFFFFF" },
        },
        screens: {
          Home: {
            screen: HomeScreen,
            linking: "",
          },
          Profile: {
            screen: ProfileScreen,
            linking: {
              path: "profile/:userId",
              parse: { userId: String },
            },
          },
          Settings: {
            screen: SettingsScreen,
            options: { title: "App Settings" },
            linking: "settings",
          },
        },
      });
      
      // Create the navigation component (wraps NavigationContainer)
      const Navigation = createStaticNavigation(RootStack);
      
      // Infer types from the static config
      type RootStackParamList = StaticParamList<typeof RootStack>;
      
      // CRITICAL: Global declaration makes useNavigation() type-safe everywhere
      declare global {
        namespace ReactNavigation {
          interface RootParamList extends RootStackParamList {}
        }
      }
      
      export function App() {
        return (
          <Navigation
            linking={{
              enabled: "auto",
              prefixes: ["myapp://", "https://myapp.com"],
            }}
          />
        );
      }
      ```
      
      ---
      
      ## Pattern 2: StaticScreenProps for Screen Components
      
      ```typescript
      // screens/profile-screen.tsx
      import { View, Text } from "react-native";
      import { useNavigation } from "@react-navigation/native";
      import type { StaticScreenProps } from "@react-navigation/native";
      
      // StaticScreenProps infers route.params type from the static config
      type Props = StaticScreenProps<{ userId: string }>;
      
      export function ProfileScreen({ route }: Props) {
        const { userId } = route.params;
        const navigation = useNavigation();
      
        const handleGoToSettings = () => {
          // Type-safe: "Settings" validated against global RootParamList
          navigation.navigate("Settings");
        };
      
        return (
          <View>
            <Text>User: {userId}</Text>
          </View>
        );
      }
      ```
      
      ---
      
      ## Pattern 3: Static API with Conditional Groups (Auth Flow)
      
      ```typescript
      import { createStaticNavigation } from "@react-navigation/native";
      import { createNativeStackNavigator } from "@react-navigation/native-stack";
      import { useContext } from "react";
      import { AuthContext } from "./auth-context";
      
      // Hook callbacks for conditional rendering
      const useIsAuthenticated = () => {
        const { isAuthenticated } = useContext(AuthContext);
        return isAuthenticated;
      };
      
      const useIsGuest = () => !useIsAuthenticated();
      
      const RootStack = createNativeStackNavigator({
        screens: {
          // Screens always visible (e.g., splash, onboarding)
        },
        groups: {
          Auth: {
            if: useIsGuest,
            screenOptions: { headerShown: false, animation: "fade" },
            screens: {
              Login: LoginScreen,
              Register: RegisterScreen,
              ForgotPassword: {
                screen: ForgotPasswordScreen,
                linking: "forgot-password",
              },
            },
          },
          Main: {
            if: useIsAuthenticated,
            screenOptions: { headerShown: true },
            screens: {
              Home: { screen: HomeScreen, linking: "" },
              Profile: {
                screen: ProfileScreen,
                linking: "profile/:userId",
              },
            },
          },
        },
      });
      ```
      
      ---
      
      ## Pattern 4: Static API with `.with()` for Dynamic Props
      
      ```typescript
      // Use .with() when the navigator needs access to hooks or providers
      const RootStack = createNativeStackNavigator({
        screens: {
          Home: HomeScreen,
          Profile: ProfileScreen,
        },
      }).with(({ Navigator }) => {
        const user = useCurrentUser();
      
        return (
          <Navigator
            screenOptions={({ route }) => {
              if (route.name === "Profile") {
                return {
                  headerRight: () =>
                    user.id === route.params.userId ? <EditButton /> : null,
                };
              }
              return {};
            }}
          />
        );
      });
      ```
      
      ---
      
      ## Pattern 5: Complete Dynamic API Setup
      
      ```typescript
      // navigation/types.ts
      import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
      import type {
        CompositeNavigationProp,
        RouteProp,
      } from "@react-navigation/native";
      import type { BottomTabNavigationProp } from "@react-navigation/bottom-tabs";
      import type { NavigatorScreenParams } from "@react-navigation/native";
      
      export type AuthStackParamList = {
        Login: undefined;
        Register: undefined;
        ForgotPassword: { email?: string };
      };
      
      export type HomeStackParamList = {
        HomeScreen: undefined;
        ProductDetail: { productId: string };
      };
      
      export type MainTabParamList = {
        HomeTab: NavigatorScreenParams<HomeStackParamList>;
        Search: { query?: string } | undefined;
        Profile: undefined;
      };
      
      export type RootStackParamList = {
        Auth: NavigatorScreenParams<AuthStackParamList>;
        Main: NavigatorScreenParams<MainTabParamList>;
        Modal: { title: string };
      };
      
      // CRITICAL: Global declaration
      declare global {
        namespace ReactNavigation {
          interface RootParamList extends RootStackParamList {}
        }
      }
      
      // Composite type for screens nested in HomeTab > MainTab > RootStack
      export type HomeScreenNavigationProp = CompositeNavigationProp<
        NativeStackNavigationProp<HomeStackParamList, "HomeScreen">,
        CompositeNavigationProp<
          BottomTabNavigationProp<MainTabParamList>,
          NativeStackNavigationProp<RootStackParamList>
        >
      >;
      ```
      
      ```typescript
      // navigation/root-navigator.tsx
      import { NavigationContainer } from "@react-navigation/native";
      import { createNativeStackNavigator } from "@react-navigation/native-stack";
      import type { RootStackParamList } from "./types";
      
      const Stack = createNativeStackNavigator<RootStackParamList>();
      
      export function RootNavigator() {
        const { isAuthenticated, isLoading } = useAuth();
      
        if (isLoading) return <SplashScreen />;
      
        return (
          <NavigationContainer>
            <Stack.Navigator screenOptions={{ headerShown: false }}>
              {isAuthenticated ? (
                <Stack.Screen name="Main" component={MainNavigator} />
              ) : (
                <Stack.Screen name="Auth" component={AuthNavigator} />
              )}
              <Stack.Group screenOptions={{ presentation: "modal" }}>
                <Stack.Screen
                  name="Modal"
                  component={ModalScreen}
                  options={({ route }) => ({ title: route.params.title })}
                />
              </Stack.Group>
            </Stack.Navigator>
          </NavigationContainer>
        );
      }
      ```
      
      ---
      
      ## Pattern 6: Typed Navigation Hooks (Dynamic API)
      
      ```typescript
      // navigation/hooks.ts
      import { useNavigation, useRoute } from "@react-navigation/native";
      import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
      import type { RouteProp } from "@react-navigation/native";
      import type { HomeStackParamList, AuthStackParamList } from "./types";
      
      // Per-navigator typed hooks -- useful when you need navigator-specific methods
      export function useHomeNavigation() {
        return useNavigation<NativeStackNavigationProp<HomeStackParamList>>();
      }
      
      export function useAuthNavigation() {
        return useNavigation<NativeStackNavigationProp<AuthStackParamList>>();
      }
      
      // Generic typed route hook
      export function useTypedRoute<
        ParamList extends Record<string, object | undefined>,
        RouteName extends keyof ParamList,
      >() {
        return useRoute<RouteProp<ParamList, RouteName>>();
      }
      ```
      
      ```typescript
      // screens/product-detail-screen.tsx
      import { useHomeNavigation, useTypedRoute } from "../navigation/hooks";
      import type { HomeStackParamList } from "../navigation/types";
      
      export function ProductDetailScreen() {
        const navigation = useHomeNavigation();
        const route = useTypedRoute<HomeStackParamList, "ProductDetail">();
      
        const { productId } = route.params; // typed as string
      
        const handleBack = () => {
          navigation.popTo("HomeScreen"); // v7: use popTo, not navigate
        };
      }
      ```
      
      ---
      
      ## Pattern 7: Combining Static and Dynamic APIs
      
      ```typescript
      // Static nested navigator
      const HomeTabs = createBottomTabNavigator({
        screens: {
          Latest: LatestScreen,
          Popular: PopularScreen,
        },
      });
      
      // Extract component for use in dynamic parent
      const HomeTabsComponent = HomeTabs.getComponent();
      
      // Generate linking config from static navigator
      import { createPathConfigForStaticNavigation } from "@react-navigation/native";
      const homeTabsLinkingScreens = createPathConfigForStaticNavigation(HomeTabs);
      
      // Dynamic parent navigator
      const RootStack = createNativeStackNavigator<RootStackParamList>();
      
      export function RootNavigator() {
        return (
          <NavigationContainer
            linking={{
              prefixes: ["myapp://"],
              config: {
                screens: {
                  Home: {
                    path: "home",
                    screens: homeTabsLinkingScreens,
                  },
                },
              },
            }}
          >
            <RootStack.Navigator>
              <RootStack.Screen name="Home" component={HomeTabsComponent} />
            </RootStack.Navigator>
          </NavigationContainer>
        );
      }
      ```
      
      **When to combine:** Incremental migration from v6 dynamic API to v7 static API. Convert one navigator at a time using `getComponent()` and `createPathConfigForStaticNavigation()`.
      
    • patterns.md 9 KB
      # React Navigation - Navigation Patterns
      
      > Auth flows, deep linking, modals, and tab+stack composition. See [core.md](core.md) for API setup and type safety.
      
      ---
      
      ## Pattern 1: Authentication Flow (Dynamic API)
      
      ```typescript
      // navigation/root-navigator.tsx
      import { NavigationContainer } from "@react-navigation/native";
      import { createNativeStackNavigator } from "@react-navigation/native-stack";
      import { useAuth } from "../hooks/use-auth";
      import type { RootStackParamList } from "./types";
      
      const Stack = createNativeStackNavigator<RootStackParamList>();
      
      export function RootNavigator() {
        const { isAuthenticated, isLoading } = useAuth();
      
        // Show splash while checking auth state (token validation, etc.)
        if (isLoading) return <SplashScreen />;
      
        return (
          <NavigationContainer>
            <Stack.Navigator screenOptions={{ headerShown: false }}>
              {isAuthenticated ? (
                <>
                  <Stack.Screen name="Main" component={MainNavigator} />
                  <Stack.Group screenOptions={{ presentation: "modal" }}>
                    <Stack.Screen name="Modal" component={ModalScreen} />
                  </Stack.Group>
                </>
              ) : (
                <Stack.Screen name="Auth" component={AuthNavigator} />
              )}
            </Stack.Navigator>
          </NavigationContainer>
        );
      }
      ```
      
      **Why this pattern:** React Navigation detects the screen list change and automatically animates the transition. No manual navigation calls needed -- just toggle the auth state and the UI follows.
      
      **Anti-pattern:** Don't conditionally render individual screens in the same navigator based on auth state. Separate auth and main into distinct navigator branches.
      
      ---
      
      ## Pattern 2: Tab Navigator with Nested Stacks
      
      ```typescript
      // navigation/main-navigator.tsx
      import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
      import { createNativeStackNavigator } from "@react-navigation/native-stack";
      import type { MainTabParamList, HomeStackParamList } from "./types";
      
      // Stack nested inside a tab
      const HomeStack = createNativeStackNavigator<HomeStackParamList>();
      
      function HomeStackNavigator() {
        return (
          <HomeStack.Navigator>
            <HomeStack.Screen
              name="HomeScreen"
              component={HomeScreen}
              options={{ headerShown: false }}
            />
            <HomeStack.Screen
              name="ProductDetail"
              component={ProductDetailScreen}
              options={{ title: "Product Details" }}
            />
          </HomeStack.Navigator>
        );
      }
      
      const Tab = createBottomTabNavigator<MainTabParamList>();
      
      const TAB_ACTIVE_COLOR = "#007AFF";
      const TAB_INACTIVE_COLOR = "#8E8E93";
      
      export function MainNavigator() {
        return (
          <Tab.Navigator
            screenOptions={{
              headerShown: false,
              tabBarActiveTintColor: TAB_ACTIVE_COLOR,
              tabBarInactiveTintColor: TAB_INACTIVE_COLOR,
            }}
          >
            <Tab.Screen
              name="HomeTab"
              component={HomeStackNavigator}
              options={{
                tabBarLabel: "Home",
                tabBarIcon: ({ color, size }) => (
                  <HomeIcon color={color} size={size} />
                ),
              }}
            />
            <Tab.Screen
              name="Search"
              component={SearchScreen}
              options={{
                tabBarLabel: "Search",
                tabBarIcon: ({ color, size }) => (
                  <SearchIcon color={color} size={size} />
                ),
              }}
            />
            <Tab.Screen
              name="Profile"
              component={ProfileScreen}
              options={{
                tabBarLabel: "Profile",
                tabBarIcon: ({ color, size }) => (
                  <ProfileIcon color={color} size={size} />
                ),
              }}
            />
          </Tab.Navigator>
        );
      }
      ```
      
      ---
      
      ## Pattern 3: Modal Navigation
      
      ```typescript
      // Modals are stack screens with presentation: "modal" or "formSheet"
      const Stack = createNativeStackNavigator<RootStackParamList>();
      
      export function RootNavigator() {
        return (
          <Stack.Navigator>
            {/* Regular screens */}
            <Stack.Group screenOptions={{ headerShown: false }}>
              <Stack.Screen name="Main" component={MainNavigator} />
            </Stack.Group>
      
            {/* Modal screens -- accessible from anywhere in the app */}
            <Stack.Group screenOptions={{ presentation: "modal", headerShown: true }}>
              <Stack.Screen
                name="CreatePost"
                component={CreatePostScreen}
                options={{ title: "New Post" }}
              />
            </Stack.Group>
      
            {/* Form sheet -- iOS bottom sheet, Android modal */}
            <Stack.Group screenOptions={{ presentation: "formSheet" }}>
              <Stack.Screen
                name="Filter"
                component={FilterScreen}
                options={{
                  sheetAllowedDetents: [0.5, 1.0],
                  sheetGrabberVisible: true,
                  sheetCornerRadius: 16,
                }}
              />
            </Stack.Group>
          </Stack.Navigator>
        );
      }
      
      // Opening modal from any screen
      function SomeScreen() {
        const navigation = useNavigation();
      
        return (
          <Pressable onPress={() => navigation.navigate("CreatePost")}>
            <Text>New Post</Text>
          </Pressable>
        );
      }
      ```
      
      **Note:** Screens pushed on top of a modal in v7 automatically use modal presentation. Set `presentation: "card"` explicitly to override this.
      
      ---
      
      ## Pattern 4: Drawer Navigator
      
      ```typescript
      import { createDrawerNavigator } from "@react-navigation/drawer";
      
      type DrawerParamList = {
        Home: undefined;
        Settings: undefined;
        About: undefined;
      };
      
      const Drawer = createDrawerNavigator<DrawerParamList>();
      
      export function DrawerNavigator() {
        return (
          <Drawer.Navigator
            screenOptions={{
              drawerActiveTintColor: "#007AFF",
              headerShown: true,
            }}
          >
            <Drawer.Screen
              name="Home"
              component={HomeScreen}
              options={{
                drawerLabel: "Home",
                drawerIcon: ({ color, size }) => (
                  <HomeIcon color={color} size={size} />
                ),
              }}
            />
            <Drawer.Screen name="Settings" component={SettingsScreen} />
            <Drawer.Screen name="About" component={AboutScreen} />
          </Drawer.Navigator>
        );
      }
      ```
      
      **Note:** Drawer navigator in v7 requires Reanimated 2 or 3 on native platforms.
      
      ---
      
      ## Pattern 5: Deep Linking (Dynamic API)
      
      ```typescript
      // navigation/linking.ts
      import type { LinkingOptions } from "@react-navigation/native";
      import type { RootStackParamList } from "./types";
      
      export const linking: LinkingOptions<RootStackParamList> = {
        prefixes: ["myapp://", "https://myapp.com"],
      
        config: {
          screens: {
            Auth: {
              screens: {
                Login: "login",
                Register: "register",
                ForgotPassword: "forgot-password",
              },
            },
            Main: {
              screens: {
                HomeTab: {
                  screens: {
                    HomeScreen: "",
                    ProductDetail: "product/:productId",
                  },
                },
                Search: "search",
                Profile: "profile",
              },
            },
            Modal: "modal/:title",
          },
        },
      };
      
      // app.tsx
      import { linking } from "./navigation/linking";
      
      export function App() {
        return (
          <NavigationContainer linking={linking} fallback={<SplashScreen />}>
            <RootNavigator />
          </NavigationContainer>
        );
      }
      ```
      
      ---
      
      ## Pattern 6: Deep Linking with Custom URL Handlers
      
      Override the default URL handling for push notifications or other custom URL sources.
      
      ```typescript
      import { Linking } from "react-native";
      import type { LinkingOptions } from "@react-navigation/native";
      
      export const linking: LinkingOptions<RootStackParamList> = {
        prefixes: ["myapp://", "https://myapp.com"],
      
        // Handle initial URL (app opened via deep link)
        async getInitialURL() {
          // Check for standard deep link first
          const url = await Linking.getInitialURL();
          if (url != null) return url;
      
          // Check push notification data as fallback
          const notification = await getLastNotificationResponse();
          return notification?.data?.url ?? null;
        },
      
        // Subscribe to incoming URLs while app is running
        subscribe(listener) {
          // Standard deep link listener
          const linkingSub = Linking.addEventListener("url", ({ url }) => {
            listener(url);
          });
      
          // Push notification URL listener
          const notifSub = addNotificationResponseListener((response) => {
            const url = response.notification.request.content.data.url;
            if (url) listener(url);
          });
      
          return () => {
            linkingSub.remove();
            notifSub.remove();
          };
        },
      
        config: {
          screens: {
            // ... screen config
          },
        },
      };
      ```
      
      ---
      
      ## Pattern 7: Navigate to Nested Screens (v7 Change)
      
      ```typescript
      // v7: Must target the parent screen, then specify the nested screen
      function goToProductDetail(productId: string) {
        // Correct: explicit parent targeting
        navigation.navigate("Main", {
          screen: "HomeTab",
          params: {
            screen: "ProductDetail",
            params: { productId },
          },
        });
      }
      
      // Anti-pattern in v7: implicit nested navigation removed
      // navigation.navigate("ProductDetail", { productId }); // WILL NOT WORK
      ```
      
      **Temporary escape hatch:** Add `navigationInChildEnabled` to `NavigationContainer` during migration. Remove once all call sites are updated.
      
      ```typescript
      <NavigationContainer navigationInChildEnabled>
        {/* ... */}
      </NavigationContainer>
      ```
      
  • reference.md 10.2 KB
    # React Navigation Quick Reference
    
    > Decision frameworks, screen options cheat sheet, v6-to-v7 migration. See [SKILL.md](SKILL.md) for red flags and philosophy.
    
    ---
    
    ## Decision Framework
    
    ### Static vs Dynamic API
    
    ```
    Starting fresh?
    |-- YES --> Static API (simpler types, auto deep linking)
    |
    Migrating from v6?
    |-- Incrementally --> Dynamic root + static nested (one at a time)
    |-- Full rewrite --> Static API
    |
    Need runtime-dynamic screen lists?
    |-- YES --> Dynamic API
    |-- NO  --> Static API
    ```
    
    ### Navigator Type
    
    ```
    What navigation pattern?
    |-- Linear forward/back flow        --> Stack Navigator
    |-- Persistent bottom bar            --> Bottom Tab Navigator
    |-- Side menu / drawer               --> Drawer Navigator
    |-- Full-screen overlay              --> Stack + presentation: "modal"
    |-- Bottom sheet / partial overlay   --> Stack + presentation: "formSheet"
    |-- Combination                      --> Nest navigators
    ```
    
    ### Navigation Method (v7)
    
    | Method                       | When to Use                                                   |
    | ---------------------------- | ------------------------------------------------------------- |
    | `navigate("Screen", params)` | Go forward to a screen. Stays put if already focused.         |
    | `popTo("Screen", params)`    | Go BACK to a specific screen in the stack. NEW in v7.         |
    | `goBack()`                   | Go back one screen.                                           |
    | `push("Screen", params)`     | Always push a new instance (even if screen already in stack). |
    | `replace("Screen", params)`  | Replace current screen (no back navigation to it).            |
    | `pop(n)`                     | Go back n screens.                                            |
    | `popToTop()`                 | Go back to first screen in stack.                             |
    | `reset({ routes: [...] })`   | Reset entire navigation state.                                |
    | `preload("Screen", params)`  | Render screen off-screen in background. NEW in v7.            |
    
    ---
    
    ## Screen Options Cheat Sheet (Native Stack)
    
    ### Header
    
    | Option                | Type               | Notes                                        |
    | --------------------- | ------------------ | -------------------------------------------- |
    | `headerShown`         | boolean            | Show/hide entire header                      |
    | `title`               | string             | Fallback for headerTitle                     |
    | `headerTitle`         | string or function | Title content                                |
    | `headerTintColor`     | string             | Back button and title color                  |
    | `headerStyle`         | object             | `{ backgroundColor }`                        |
    | `headerTransparent`   | boolean            | Transparent header background                |
    | `headerBlurEffect`    | string             | iOS blur material ("regular", "light", etc.) |
    | `headerShadowVisible` | boolean            | Show header bottom shadow                    |
    | `headerLeft`          | function           | Custom left element                          |
    | `headerRight`         | function           | Custom right element                         |
    
    ### Large Title (iOS)
    
    | Option                          | Type    | Notes                                         |
    | ------------------------------- | ------- | --------------------------------------------- |
    | `headerLargeTitleEnabled`       | boolean | Enable collapsible large title                |
    | `headerLargeStyle`              | object  | `{ backgroundColor }` for large title area    |
    | `headerLargeTitleStyle`         | object  | `{ fontFamily, fontSize, fontWeight, color }` |
    | `headerLargeTitleShadowVisible` | boolean | Shadow below large title                      |
    
    ### Back Button (iOS)
    
    | Option                        | Type    | Notes                                                                           |
    | ----------------------------- | ------- | ------------------------------------------------------------------------------- |
    | `headerBackVisible`           | boolean | Show/hide back button                                                           |
    | `headerBackTitle`             | string  | Custom back button label                                                        |
    | `headerBackButtonDisplayMode` | string  | `"default"`, `"generic"`, or `"minimal"` (replaces v6 `headerBackTitleVisible`) |
    | `headerBackButtonMenuEnabled` | boolean | Long-press shows stack history (default: true)                                  |
    
    ### Search Bar
    
    | Option                                     | Type     | Notes                        |
    | ------------------------------------------ | -------- | ---------------------------- |
    | `headerSearchBarOptions.placeholder`       | string   | Search placeholder text      |
    | `headerSearchBarOptions.onChangeText`      | function | Text change handler          |
    | `headerSearchBarOptions.hideWhenScrolling` | boolean  | iOS: collapse on scroll      |
    | `headerSearchBarOptions.autoFocus`         | boolean  | Android: auto-focus on mount |
    
    ### Animation
    
    | Option                     | Type    | Notes                                                                                                                                              |
    | -------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
    | `animation`                | string  | `"default"`, `"fade"`, `"slide_from_right"`, `"slide_from_left"`, `"slide_from_bottom"`, `"fade_from_bottom"`, `"flip"`, `"simple_push"`, `"none"` |
    | `animationDuration`        | number  | iOS only, in ms (default: 350)                                                                                                                     |
    | `gestureEnabled`           | boolean | iOS: swipe back gesture                                                                                                                            |
    | `fullScreenGestureEnabled` | boolean | iOS: swipe from anywhere                                                                                                                           |
    
    ### Presentation
    
    | Option                | Type    | Notes                                                                                                                            |
    | --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- |
    | `presentation`        | string  | `"card"`, `"modal"`, `"transparentModal"`, `"containedModal"`, `"fullScreenModal"`, `"formSheet"`, `"containedTransparentModal"` |
    | `sheetAllowedDetents` | array   | Form sheet stop points: `[0.25, 0.5, 1.0]` or `"fitToContents"`                                                                  |
    | `sheetGrabberVisible` | boolean | iOS: show drag indicator                                                                                                         |
    | `sheetCornerRadius`   | number  | Corner radius in points                                                                                                          |
    
    ### Performance
    
    | Option         | Type    | Notes                                        |
    | -------------- | ------- | -------------------------------------------- |
    | `freezeOnBlur` | boolean | Prevents re-renders when screen inactive     |
    | `lazy`         | boolean | Tab/Drawer: don't render until first visited |
    
    ---
    
    ## v6 to v7 Migration Checklist
    
    ### Breaking Changes
    
    - [ ] `navigate()` no longer pops back -- replace with `popTo()` for back navigation
    - [ ] Implicit nested navigation removed -- use `navigate("Parent", { screen: "Child" })`
    - [ ] `headerBackTitleVisible` removed -- use `headerBackButtonDisplayMode: "minimal"`
    - [ ] `animationEnabled: false` removed -- use `animation: "none"`
    - [ ] `unmountOnBlur` removed from tabs/drawer -- use `popToTopOnBlur: true`
    - [ ] Custom theme requires `fonts` property -- spread `DefaultTheme`
    - [ ] `<Link to="/path">` changed to `<Link screen="Name" params={...}>`
    - [ ] `independent` prop removed -- wrap in `<NavigationIndependentTree>`
    - [ ] `sceneContainerStyle` removed -- use `sceneStyle` in `screenOptions`
    - [ ] Material Bottom Tabs moved to `react-native-paper/react-navigation`
    - [ ] Flipper plugin removed -- use `useLogger` hook or DevTools extension
    - [ ] `react-native-screens` v4 required for native stack
    - [ ] Drawer requires Reanimated 2 or 3
    
    ### New Features Available
    
    - [ ] Static API for simpler TypeScript and auto deep linking
    - [ ] `preload()` for background screen rendering
    - [ ] `usePreventRemove()` for unsaved changes guards
    - [ ] `layout` prop on navigators, screens, and groups
    - [ ] `headerSearchBarOptions` on all header-supporting navigators
    - [ ] Bottom Tab `tabBarPosition: "left"` or `"right"` for sidebar layout
    - [ ] Bottom Tab `animation` for tab transition animations
    - [ ] `popTo()` for explicit back navigation
    - [ ] Form sheet presentation with detents
    
    ### Temporary Migration Helpers
    
    - `navigateDeprecated()` -- maintains v6 navigate() behavior
    - `navigationInChildEnabled` prop -- maintains implicit nested navigation
    - Remove these once migration is complete
    
    ---
    
    ## Essential Imports
    
    ```typescript
    // Core
    import {
      NavigationContainer,
      createStaticNavigation,
      useNavigation,
      useRoute,
      useFocusEffect,
      usePreventRemove,
      useIsFocused,
      NavigationIndependentTree,
    } from "@react-navigation/native";
    
    import type {
      StaticParamList,
      StaticScreenProps,
      NavigatorScreenParams,
      CompositeScreenProps,
      CompositeNavigationProp,
      LinkingOptions,
      NavigationState,
      Theme,
    } from "@react-navigation/native";
    
    // Native Stack
    import { createNativeStackNavigator } from "@react-navigation/native-stack";
    import type {
      NativeStackNavigationProp,
      NativeStackScreenProps,
    } from "@react-navigation/native-stack";
    
    // Bottom Tabs
    import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
    import type {
      BottomTabNavigationProp,
      BottomTabScreenProps,
    } from "@react-navigation/bottom-tabs";
    
    // Drawer
    import { createDrawerNavigator } from "@react-navigation/drawer";
    import type {
      DrawerNavigationProp,
      DrawerScreenProps,
    } from "@react-navigation/drawer";
    ```
    
  • SKILL.md 19.1 KB
    ---
    name: mobile-navigation-react-navigation
    description: React Navigation 7+ patterns - static and dynamic APIs, type-safe navigation, stack/tab/drawer navigators, deep linking, authentication flows, screen preloading, header customization
    ---
    
    # React Navigation Patterns
    
    > **Quick Guide:** Use the static API for simpler TypeScript inference and automatic deep linking config. Use the dynamic API when you need runtime-dynamic screen lists. Always declare a global `RootParamList` for type-safe `useNavigation` everywhere. Use `createNativeStackNavigator` (not the JS stack) for production performance. Auth flows use conditional screen rendering via the `if` callback (static) or conditional JSX (dynamic). Deep linking config lives per-screen in the static API -- no separate config object needed.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST declare a global `ReactNavigation.RootParamList` interface so `useNavigation` is type-safe without manual annotation)**
    
    **(You MUST use `createNativeStackNavigator` for production apps -- the JS stack (`@react-navigation/stack`) is significantly slower and only needed for highly custom transitions)**
    
    **(You MUST use `popTo()` to navigate back to a previous screen in the stack -- `navigate()` in v7 no longer pops back to existing screens)**
    
    **(You MUST wrap `useFocusEffect` callbacks in `useCallback` -- without it, the effect runs on every render, not just focus changes)**
    
    **(You MUST NOT use `navigation.navigate('NestedScreen')` to reach screens in child navigators -- v7 removed implicit nested navigation; use explicit parent targeting)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** React Navigation, @react-navigation, createNativeStackNavigator, createBottomTabNavigator, createDrawerNavigator, createStaticNavigation, NavigationContainer, useNavigation, useRoute, useFocusEffect, usePreventRemove, StaticParamList, StaticScreenProps, NativeStackNavigationProp, CompositeNavigationProp, NavigatorScreenParams, deep linking, linking config, headerSearchBarOptions, headerLargeTitle, popTo, preload
    
    **When to use:**
    
    - Setting up navigation structure (stack, tab, drawer) in a React Native app
    - Choosing between static API and dynamic API for navigator configuration
    - Adding type-safe navigation with TypeScript (param lists, typed hooks)
    - Configuring deep linking (URL prefixes, path params, universal links)
    - Implementing authentication flows with conditional screen rendering
    - Customizing headers (large titles, search bars, custom buttons)
    - Preloading screens for perceived performance
    - Preventing back navigation for unsaved changes
    
    **When NOT to use:**
    
    - File-based routing with a managed workflow (uses its own router built on React Navigation)
    - Web-only React apps (use a web router)
    - Simple single-screen apps with no navigation
    
    **Key patterns covered:**
    
    - Static API vs dynamic API: when to use each
    - Global `RootParamList` declaration for type-safe hooks everywhere
    - Native stack vs JS stack performance trade-offs
    - Auth flow with conditional screens (static `if` callback or dynamic JSX)
    - Deep linking configuration (per-screen in static, `linking` prop in dynamic)
    - Screen preloading with `navigation.preload()`
    - `useFocusEffect` for screen lifecycle management
    - `usePreventRemove` for unsaved changes guards
    - Header customization: large titles, search bars, form sheets
    
    **Detailed Resources:**
    
    - [examples/core.md](examples/core.md) - Static API setup, dynamic API setup, type-safe navigation, global RootParamList
    - [examples/patterns.md](examples/patterns.md) - Auth flows, deep linking, modals, tab navigator with nested stacks
    - [examples/advanced.md](examples/advanced.md) - Screen preloading, state persistence, usePreventRemove, useFocusEffect, header customization
    - [reference.md](reference.md) - Decision frameworks, screen options cheat sheet, v6-to-v7 migration
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    React Navigation provides routing and navigation for React Native apps. The key decision in v7 is **static vs dynamic API**:
    
    - **Static API** -- object-based configuration. Simpler TypeScript (types inferred from config), automatic deep linking path generation, less boilerplate. Use for most apps.
    - **Dynamic API** -- component-based configuration (`<Stack.Navigator>`/`<Stack.Screen>`). Required when screen lists change at runtime or you need full programmatic control over navigator props. More verbose but more flexible.
    
    Both APIs produce the same navigation behavior -- the difference is configuration ergonomics.
    
    **Core principles:**
    
    1. **Native stack by default** -- `createNativeStackNavigator` uses platform navigation primitives (UINavigationController/Fragment) for smoother transitions and lower memory. The JS stack (`@react-navigation/stack`) only when you need custom transition animations not available natively.
    2. **Type safety from the root** -- Declare `ReactNavigation.RootParamList` globally so every `useNavigation()` call is type-checked without manual generics.
    3. **Deep linking as first-class** -- Configure linking per-screen (static API) or in a centralized config (dynamic API). Prefixes handle custom schemes and universal links.
    4. **Screen lifecycle via focus** -- Screens in a stack remain mounted when covered. Use `useFocusEffect` (not `useEffect`) for work that should pause when the screen loses focus.
    
    **v7 behavioral changes from v6:**
    
    - `navigate()` no longer pops back to existing screens -- use `popTo()` instead
    - Implicit nested navigator navigation removed -- must target parent screen explicitly
    - `headerBackTitleVisible` replaced with `headerBackButtonDisplayMode`
    - Navigation state is frozen in dev mode (mutations throw)
    - Theme objects now require a `fonts` property
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Static API Setup
    
    The static API uses object configuration for simpler TypeScript and automatic deep linking.
    
    ```typescript
    import { createStaticNavigation } from "@react-navigation/native";
    import { createNativeStackNavigator } from "@react-navigation/native-stack";
    import type { StaticParamList } from "@react-navigation/native";
    
    const RootStack = createNativeStackNavigator({
      initialRouteName: "Home",
      screenOptions: { headerShown: true },
      screens: {
        Home: HomeScreen,
        Profile: {
          screen: ProfileScreen,
          linking: "profile/:userId",
        },
      },
    });
    
    const Navigation = createStaticNavigation(RootStack);
    
    // Declare global types -- makes useNavigation() type-safe everywhere
    type RootStackParamList = StaticParamList<typeof RootStack>;
    declare global {
      namespace ReactNavigation {
        interface RootParamList extends RootStackParamList {}
      }
    }
    
    export function App() {
      return <Navigation />;
    }
    ```
    
    **Why good:** types inferred from config (no manual `ParamList`), deep linking paths defined per-screen, less boilerplate than dynamic API
    
    See [examples/core.md](examples/core.md) for complete static API setup with groups and conditional screens.
    
    ---
    
    ### Pattern 2: Dynamic API Setup
    
    The dynamic API uses JSX components. Use when screen lists are runtime-dynamic.
    
    ```typescript
    import { NavigationContainer } from "@react-navigation/native";
    import { createNativeStackNavigator } from "@react-navigation/native-stack";
    
    type RootStackParamList = {
      Home: undefined;
      Profile: { userId: string };
    };
    
    // Must declare globally for type-safe useNavigation()
    declare global {
      namespace ReactNavigation {
        interface RootParamList extends RootStackParamList {}
      }
    }
    
    const Stack = createNativeStackNavigator<RootStackParamList>();
    
    export function App() {
      return (
        <NavigationContainer>
          <Stack.Navigator initialRouteName="Home">
            <Stack.Screen name="Home" component={HomeScreen} />
            <Stack.Screen name="Profile" component={ProfileScreen} />
          </Stack.Navigator>
        </NavigationContainer>
      );
    }
    ```
    
    **Why good:** familiar JSX pattern, supports runtime-dynamic screen lists, manual param list gives explicit control
    
    See [examples/core.md](examples/core.md) for dynamic API with typed hooks and nested navigators.
    
    ---
    
    ### Pattern 3: Type-Safe Navigation Hooks
    
    Declare `RootParamList` globally once, then `useNavigation()` and `useRoute()` are type-safe everywhere without manual generics.
    
    ```typescript
    // In any screen component -- no generic needed
    function HomeScreen() {
      const navigation = useNavigation();
    
      // Type-checked: "Profile" must exist, params must match
      navigation.navigate("Profile", { userId: "123" });
    
      // Type error: "Nonexistent" is not in RootParamList
      navigation.navigate("Nonexistent"); // compile error
    }
    ```
    
    For nested navigators, use `CompositeScreenProps` or `NavigatorScreenParams` to propagate types. With the static API, use `StaticScreenProps` for screen component props.
    
    See [examples/core.md](examples/core.md) for composite types and `StaticScreenProps`.
    
    ---
    
    ### Pattern 4: Authentication Flow
    
    Conditionally render auth or main screens. React Navigation animates the transition automatically.
    
    ```typescript
    // Static API: use the `if` callback on groups
    const useIsAuthenticated = () => {
      const { isAuthenticated } = useContext(AuthContext);
      return isAuthenticated;
    };
    
    const useIsGuest = () => !useIsAuthenticated();
    
    const RootStack = createNativeStackNavigator({
      screens: {},
      groups: {
        Auth: {
          if: useIsGuest,
          screenOptions: { headerShown: false },
          screens: { Login: LoginScreen, Register: RegisterScreen },
        },
        Main: {
          if: useIsAuthenticated,
          screens: { Home: HomeScreen, Profile: ProfileScreen },
        },
      },
    });
    ```
    
    **Why good:** `if` callbacks cleanly separate auth/main screens, React Navigation handles transition animation, no manual state-based conditional rendering needed
    
    See [examples/patterns.md](examples/patterns.md) for both static and dynamic auth flow implementations.
    
    ---
    
    ### Pattern 5: Deep Linking
    
    Static API: define `linking` per-screen. Dynamic API: pass a `linking` config to `NavigationContainer`.
    
    ```typescript
    // Static API -- linking defined inline per screen
    const RootStack = createNativeStackNavigator({
      screens: {
        Home: { screen: HomeScreen, linking: "" },
        Profile: {
          screen: ProfileScreen,
          linking: {
            path: "user/:userId",
            parse: { userId: (id: string) => id.replace(/^@/, "") },
            stringify: { userId: (id: string) => `@${id}` },
          },
        },
      },
    });
    
    const Navigation = createStaticNavigation(RootStack);
    
    export function App() {
      return (
        <Navigation
          linking={{ prefixes: ["myapp://", "https://myapp.com"] }}
        />
      );
    }
    ```
    
    **Why good:** linking config co-located with screen definition, parse/stringify handle URL encoding, prefixes handle both custom scheme and universal links
    
    See [examples/patterns.md](examples/patterns.md) for dynamic API linking, custom URL handlers, and platform-specific setup.
    
    ---
    
    ### Pattern 6: Native Stack vs JS Stack
    
    ```
    Which stack navigator?
    |-- Need custom JS-driven transition animations? --> @react-navigation/stack (JS)
    |-- Everything else --> @react-navigation/native-stack (NATIVE)
    ```
    
    | Feature            | Native Stack                       | JS Stack                   |
    | ------------------ | ---------------------------------- | -------------------------- |
    | Performance        | Native animations, lower memory    | JS-driven, higher overhead |
    | Transitions        | Platform defaults + limited custom | Fully customizable         |
    | Large titles (iOS) | Supported natively                 | Not available              |
    | Search bar (iOS)   | headerSearchBarOptions             | Must build custom          |
    | Form sheets        | presentation: "formSheet"          | Not available              |
    | Gesture handling   | Native, smooth                     | JS-driven                  |
    
    **Default to native stack.** Only use JS stack when you need transition animations that native stack cannot provide.
    
    ---
    
    ### Pattern 7: useFocusEffect for Screen Lifecycle
    
    Screens in a stack remain mounted when a new screen is pushed. Use `useFocusEffect` to run effects only when the screen is focused.
    
    ```typescript
    import { useCallback } from "react";
    import { useFocusEffect } from "@react-navigation/native";
    
    function ChatScreen({ roomId }: { roomId: string }) {
      useFocusEffect(
        useCallback(() => {
          const ws = new WebSocket(`wss://chat.example.com/rooms/${roomId}`);
          // Cleanup runs when screen loses focus
          return () => ws.close();
        }, [roomId]),
      );
    }
    ```
    
    **Gotcha:** The callback MUST be wrapped in `useCallback`. Without it, the effect re-runs on every render, not just focus changes.
    
    See [examples/advanced.md](examples/advanced.md) for polling, analytics tracking, and resource cleanup patterns.
    
    ---
    
    ### Pattern 8: Screen Preloading
    
    Preload heavy screens before the user navigates to them. The screen is rendered off-screen with all hooks running.
    
    ```typescript
    function ProductList() {
      const navigation = useNavigation();
    
      const handleLongPress = (productId: string) => {
        navigation.preload("ProductDetail", { productId });
      };
      // Later: navigation.navigate("ProductDetail", { productId }) is instant
    }
    ```
    
    **Limitations:** Preloaded screens cannot dispatch navigation actions, update options, or listen to events until actually navigated to.
    
    ---
    
    ### Pattern 9: Header Customization
    
    Native stack supports platform-native header features: large titles, search bars, and form sheets.
    
    ```typescript
    <Stack.Screen
      name="Settings"
      component={SettingsScreen}
      options={{
        headerLargeTitleEnabled: true,
        headerLargeStyle: { backgroundColor: "#f5f5f5" },
        headerSearchBarOptions: {
          placeholder: "Search settings...",
          onChangeText: (e) => handleSearch(e.nativeEvent.text),
          hideWhenScrolling: true,
        },
      }}
    />
    ```
    
    **Gotcha:** Custom `header` functions disable ALL native header features (large title, search bar, blur effects). Use `headerLeft`/`headerRight` to add custom elements while keeping native behavior.
    
    See [examples/advanced.md](examples/advanced.md) for form sheets, custom header items, and search bar integration.
    
    </patterns>
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### Static vs Dynamic API
    
    ```
    Starting a new navigation setup?
    |-- Can all screens be defined at build time?
    |   |-- YES --> Static API (simpler TS, auto deep linking)
    |   +-- NO  --> Dynamic API (runtime screen lists)
    |
    |-- Migrating incrementally from v6?
    |   +-- YES --> Dynamic API at root, static for new navigators
    |       (use getComponent() and createPathConfigForStaticNavigation)
    |
    |-- Need to wrap navigator with providers (e.g. context)?
    |   +-- Use static API with .with() method
    ```
    
    ### Navigator Type
    
    ```
    What navigation pattern?
    |-- Linear flow (onboarding, checkout) --> Stack Navigator
    |-- Main app sections with persistent bar --> Bottom Tab Navigator
    |-- Side menu / settings panel --> Drawer Navigator
    |-- Modal overlays --> Stack with presentation: "modal"
    |-- Bottom sheets --> Stack with presentation: "formSheet"
    |-- Combination --> Nest navigators (tabs inside stack, stacks inside tabs)
    ```
    
    ### Navigation Method
    
    ```
    How to move between screens?
    |-- Push new screen forward --> navigation.navigate("Screen", params)
    |-- Go back to specific screen --> navigation.popTo("Screen", params)
    |-- Go back one screen --> navigation.goBack()
    |-- Replace current screen --> navigation.replace("Screen", params)
    |-- Reset entire stack --> navigation.reset({ routes: [...] })
    |-- Navigate to nested screen --> navigation.navigate("Parent", { screen: "Child" })
    ```
    
    </decision_framework>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority Issues:**
    
    - Using `navigate()` to go back to a previous screen -- v7 changed behavior; `navigate()` stays on current screen if target exists. Use `popTo()` instead.
    - Using `navigation.navigate("NestedScreen")` to reach child navigator screens -- removed in v7. Must use `navigate("ParentScreen", { screen: "NestedScreen" })`.
    - Using JS stack (`@react-navigation/stack`) for production without a specific need for custom transitions -- native stack is significantly more performant.
    - Missing global `RootParamList` declaration -- every `useNavigation()` call is untyped, losing the primary benefit of TypeScript with React Navigation.
    - Using a custom `header` function and expecting native features (large title, search bar, blur) -- custom headers disable all native header functionality.
    
    **Medium Priority Issues:**
    
    - Inline component functions in `<Stack.Screen component={() => <MyScreen />} />` -- creates a new component on every render, causing unmount/remount. Always pass a reference.
    - Not using `useFocusEffect` for screen-specific side effects -- `useEffect` runs even when the screen is covered by another screen in the stack.
    - Mutating navigation state directly (caught in dev mode in v7, silent corruption in prod).
    - Missing `fonts` property in custom theme -- required in v7, crashes without it.
    
    **Gotchas & Edge Cases:**
    
    - `useFocusEffect` callback must be wrapped in `useCallback` -- without it, the effect fires on every render, not just focus changes
    - `usePreventRemove` only fires for navigation state removal (back, pop, reset) -- it does NOT fire when the screen is merely unfocused (push, tab switch)
    - Preloaded screens cannot dispatch navigation actions or call `navigation.setOptions()` until actually navigated to
    - Screen `options` can be an object or a function receiving `{ route, navigation }` -- use the function form when options depend on route params
    - `headerSearchBarOptions` requires `contentInsetAdjustmentBehavior="automatic"` on your ScrollView/FlatList for proper layout
    - `headerBackButtonDisplayMode` replaced `headerBackTitleVisible` in v7 -- values are "default", "generic", or "minimal"
    - `unmountOnBlur` removed from tabs/drawer in v7 -- use `popToTopOnBlur: true` or the `useIsFocused` pattern instead
    - Navigation state frozen in dev mode -- if you were mutating state directly, you'll get runtime errors in v7 dev builds
    - Android requires `RNScreensFragmentFactory` setup in `MainActivity` -- without it, View state is lost during Activity restarts
    - The `Link` component changed from path-based to screen-based: `<Link screen="Profile" params={{ userId }}>` not `<Link to="/profile/123">`
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md**
    
    **(You MUST declare a global `ReactNavigation.RootParamList` interface so `useNavigation` is type-safe without manual annotation)**
    
    **(You MUST use `createNativeStackNavigator` for production apps -- the JS stack (`@react-navigation/stack`) is significantly slower and only needed for highly custom transitions)**
    
    **(You MUST use `popTo()` to navigate back to a previous screen in the stack -- `navigate()` in v7 no longer pops back to existing screens)**
    
    **(You MUST wrap `useFocusEffect` callbacks in `useCallback` -- without it, the effect runs on every render, not just focus changes)**
    
    **(You MUST NOT use `navigation.navigate('NestedScreen')` to reach screens in child navigators -- v7 removed implicit nested navigation; use explicit parent targeting)**
    
    **Failure to follow these rules will cause untyped navigation, performance issues, broken back navigation, and runtime errors.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related