React Navigation Patterns
SkillSecurityReact Navigation 7+ patterns - static and dynamic APIs, type-safe navigation, stack/tab/drawer navigators, deep linking, authentication flows, screen preloading, header customization
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the React Navigation Patterns skill
What this skill tells your AI
The instructions your AI receives, as published by agents-inc/skills in src/skills/mobile-navigation-react-navigation/SKILL.md and read by ahel’s review.
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
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:
- Native stack by default --
createNativeStackNavigatoruses 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. - Type safety from the root -- Declare
ReactNavigation.RootParamListglobally so everyuseNavigation()call is type-checked without manual generics. - 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.
- Screen lifecycle via focus -- Screens in a stack remain mounted when covered. Use
useFocusEffect(notuseEffect) for work that should pause when the screen loses focus.
v7 behavioral changes from v6:
navigate()no longer pops back to existing screens -- usepopTo()instead- Implicit nested navigator navigation removed -- must target parent screen explicitly
headerBackTitleVisiblereplaced withheaderBackButtonDisplayMode- Navigation state is frozen in dev mode (mutations throw)
- Theme objects now require a
fontsproperty
Core Patterns
Pattern 1: Static API Setup
The static API uses object configuration for simpler TypeScript and automatic deep linking.
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 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.
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 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.
// 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 for composite types and StaticScreenProps.
Pattern 4: Authentication Flow
Conditionally render auth or main screens. React Navigation animates the transition automatically.
// 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 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.
// 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 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.
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 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.
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.
<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 for form sheets, custom header items, and search bar integration.
<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>
Signals
- GitHub stars
- 24
- Forks
- 8
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
mobile-navigation-react-navigation- Source
- github.com/agents-inc/skills