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
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/mobile-navigation-react-navigation/skills/mobile-navigation-react-navigation
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
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
RootParamListfor type-safeuseNavigationeverywhere. UsecreateNativeStackNavigator(not the JS stack) for production performance. Auth flows use conditional screen rendering via theifcallback (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
RootParamListdeclaration for type-safe hooks everywhere - Native stack vs JS stack performance trade-offs
- Auth flow with conditional screens (static
ifcallback or dynamic JSX) - Deep linking configuration (per-screen in static,
linkingprop in dynamic) - Screen preloading with
navigation.preload() useFocusEffectfor screen lifecycle managementusePreventRemovefor 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. UsepopTo()instead. - Using
navigation.navigate("NestedScreen")to reach child navigator screens -- removed in v7. Must usenavigate("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
RootParamListdeclaration -- everyuseNavigation()call is untyped, losing the primary benefit of TypeScript with React Navigation. - Using a custom
headerfunction 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
useFocusEffectfor screen-specific side effects --useEffectruns 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
fontsproperty in custom theme -- required in v7, crashes without it.
Gotchas & Edge Cases:
useFocusEffectcallback must be wrapped inuseCallback-- without it, the effect fires on every render, not just focus changesusePreventRemoveonly 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
optionscan be an object or a function receiving{ route, navigation }-- use the function form when options depend on route params headerSearchBarOptionsrequirescontentInsetAdjustmentBehavior="automatic"on your ScrollView/FlatList for proper layoutheaderBackButtonDisplayModereplacedheaderBackTitleVisiblein v7 -- values are "default", "generic", or "minimal"unmountOnBlurremoved from tabs/drawer in v7 -- usepopToTopOnBlur: trueor theuseIsFocusedpattern instead- Navigation state frozen in dev mode -- if you were mutating state directly, you'll get runtime errors in v7 dev builds
- Android requires
RNScreensFragmentFactorysetup inMainActivity-- without it, View state is lost during Activity restarts - The
Linkcomponent 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.
Reviews (0)
No reviews yet.
No comments yet.