Skip to main content
Mobile Launcher
Learning & TutorialsMobile Development

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:

  1. Root Stack (Switch): Checks Auth State.
  2. Auth Stack: Login, Register.
  3. App Tabs: Home, Search, Profile.

Real World Example: Root Navigator

Located at src/navigation/navigators/root-stack-navigator.tsx.

Typescript
// 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:

Typescript
export type RootStackParamList = {
  Auth: undefined;
  App: undefined;
  Paywall: { from: string }; // Params example
};

Usage in components:

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

Typescript
// 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:

Typescript
// 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:

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

Typescript
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 navigate calls: always type useNavigation with the relevant param list so missing or wrong params fail at build time.