Navigation
Managing Screens with React Navigation
React Navigation Setup
We use React Navigation (v6+), the industry standard for routing in React Native.
Structure
Our navigation is hierarchical:
- Root Stack (Switch): Checks Auth State.
- Auth Stack: Login, Register.
- App Tabs: Home, Search, Profile.
Real World Example: Root Navigator
Located at src/navigation/navigators/root-stack-navigator.tsx.
// The Root Navigator decides what the user sees on launch
const RootStackNavigator = () => {
const isAuthenticated = useAppSelector(selectIsAuthenticated);
const isOnboardingComplete = useAppSelector(selectIsOnboardingCompleted);
return (
<Stack.Navigator screenOptions={{ headerShown: false }}>
{!isAuthenticated ? (
// 1. User not logged in -> Auth Flow
<Stack.Screen name="Auth" component={AuthNavigator} />
) : !isOnboardingComplete ? (
// 2. User logged in but new -> Onboarding Flow
<Stack.Screen name="Onboarding" component={OnboardingNavigator} />
) : (
// 3. User fully active -> Main App
<Stack.Screen name="App" component={AppTabNavigator} />
)}
</Stack.Navigator>
);
};Typesafe Navigation
We don't use string literals like navigation.navigate('Home') loosely. We use TypeScript.
Defined in src/navigation/routes.types.ts:
export type RootStackParamList = {
Auth: undefined;
App: undefined;
Paywall: { from: string }; // Params example
};Usage in components:
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
// TypeScript knows 'Paywall' requires 'from' param
navigation.navigate('Paywall', { from: 'settings' });The Tab Navigator
Once a user is authenticated and onboarded, the Root Navigator hands off to AppTabNavigator. This is where the bottom tab bar lives — Home, Search, and Profile in the default boilerplate. Each tab is itself a stack, so you can push detail screens inside a tab without losing the tab bar.
// src/navigation/navigators/app-tab-navigator.tsx
const AppTabNavigator = () => (
<Tab.Navigator screenOptions={{ headerShown: false }}>
<Tab.Screen name="HomeTab" component={HomeStackNavigator} />
<Tab.Screen name="SearchTab" component={SearchStackNavigator} />
<Tab.Screen name="ProfileTab" component={ProfileStackNavigator} />
</Tab.Navigator>
);Nesting a stack inside each tab is the standard React Native pattern: the tab bar stays fixed while the user drills into detail screens and back out.
Passing and Reading Params
Because the param lists are typed, the compiler enforces that you pass the right data when navigating, and reading it back is fully typed too:
// Reading params in the destination screen
type PaywallRouteProp = RouteProp<RootStackParamList, 'Paywall'>;
const PaywallScreen = () => {
const route = useRoute<PaywallRouteProp>();
const { from } = route.params; // typed as string
// ...
};This is the single biggest reason to keep RootStackParamList accurate: a typo in a route name or a missing param becomes a compile error instead of a runtime crash on a user's device.
Going Back and Resetting
For simple back navigation use navigation.goBack(). After flows like login or onboarding you usually want to replace the stack rather than let the user swipe back into it — use reset:
navigation.reset({
index: 0,
routes: [{ name: 'App' }],
});Because the Root Navigator already switches on auth and onboarding state, you rarely call reset directly — flipping the Redux flag re-renders the Root Navigator into the right branch for you.
Deep Linking
Expo handles deep links (e.g., myapp://param/123).
Configuration is in src/navigation/routes.ts.
const linking = {
prefixes: ['myapp://', 'https://myapp.com'],
config: {
screens: {
Auth: {
screens: {
Login: 'login', // myapp://login
}
}
}
}
};When a deep link arrives while the user is logged out, the Root Navigator still gates them through the Auth flow first, then resolves the intended screen — so links never bypass authentication.
Common Pitfalls
- Navigating to a screen in a different navigator: use the nested syntax —
navigation.navigate('App', { screen: 'ProfileTab' })— rather than the bare screen name. - Stale params: params persist on a route until you navigate to it again with new ones. Don't rely on a param being cleared automatically.
- Untyped
navigatecalls: always typeuseNavigationwith the relevant param list so missing or wrong params fail at build time.