DEV Community

Cover image for React Native Navigation Done Right - The Mental Model Explained
Mohammad Irfan
Mohammad Irfan

Posted on

React Native Navigation Done Right - The Mental Model Explained

If you've built more than one React Native app, you know the feeling.

You start a new project, get to the navigation setup, and suddenly 2 days are gone.

Not because you're slow. Because React Navigation has moving parts that nobody explains
together in one place.

  • Nested navigators
  • Auth switching
  • Typed routes
  • Why is the tab bar showing on this screen?
  • Why does going back take me to the wrong place?

I've been there. Every project. Same pain, different client.

So when I started building my open source React Native starter kit, I decided to
figure this out properly — once and for all — and document every decision along the way.

Here's what I learned.


The Mental Model (Start Here)

Before touching any code, you need one simple mental model.

Think of navigation like a stack of cards on a table.

Navigator Mental Model
Stack Navigator Cards stacked on top of each other. Going back removes the top card.
Tab Navigator Multiple piles side by side. Switching tabs just switches which pile you're looking at — it doesn't stack.
Nested Navigator A card in one stack that is itself an entire new stack.

That last one is where most people get lost. Once it clicks, everything else makes sense.


3 Questions Before Writing Any Code

Before building any navigation now, I ask these three questions:

1. Can the user go back from this screen?
→ If yes, it belongs in a Stack Navigator.

2. Is this screen inside or outside the tabs?
→ Screens inside the tabs go in the Tab Navigator. Screens that should cover the
tabs entirely (like a details page) go in the Stack above the tabs.

3. Does authentication affect what the user sees?
→ If yes, you need a Root Navigator that switches between an Auth stack and an App stack.

Three questions. Answer them before you write a single line.


The Folder Structure That Changed Everything

I used to dump everything into one navigation file. It worked until it didn't.

src/
└── navigation/
    ├── index.tsx              ← RootNavigator
    ├── AuthNavigator.tsx      ← Login, Register
    ├── AppNavigator.tsx       ← Authenticated screens
    ├── BottomTabNavigator.tsx ← Tab screens
    └── types.ts               ← All route params typed
Enter fullscreen mode Exit fullscreen mode

Each file has exactly one job.

When something breaks, you know which file to open. When you add a screen, you know
exactly where it goes.


Types First — Always

This is the step I used to skip. Do not skip this.

// navigation/types.ts

export type AuthStackParamList = {
  Login: undefined;
  Register: undefined;
};

export type AppStackParamList = {
  BottomTabs: undefined;
  Details: { id: string; title: string };
};

export type BottomTabParamList = {
  Home: undefined;
  Explore: undefined;
  Profile: undefined;
};
Enter fullscreen mode Exit fullscreen mode

undefined means the screen takes no params. Everything else defines exactly what
params are required.

Without this: typos in route names cause runtime crashes, missing params only
surface when a user hits that screen.

With this: TypeScript catches every mistake before the app runs.

The 10 minutes upfront saves hours of debugging.


Building the Navigators

1. AuthNavigator

// navigation/AuthNavigator.tsx

const Stack = createNativeStackNavigator<AuthStackParamList>();

const AuthNavigator = () => {
  return (
    <Stack.Navigator screenOptions={{ headerShown: false }}>
      <Stack.Screen name="Login" component={LoginScreen} />
      <Stack.Screen name="Register" component={RegisterScreen} />
    </Stack.Navigator>
  );
};
Enter fullscreen mode Exit fullscreen mode

2. BottomTabNavigator

// navigation/BottomTabNavigator.tsx

const Tab = createBottomTabNavigator<BottomTabParamList>();

const BottomTabNavigator = () => {
  return (
    <Tab.Navigator screenOptions={{ headerShown: false }}>
      <Tab.Screen name="Home" component={HomeScreen} />
      <Tab.Screen name="Explore" component={ExploreScreen} />
      <Tab.Screen name="Profile" component={ProfileScreen} />
    </Tab.Navigator>
  );
};
Enter fullscreen mode Exit fullscreen mode

3. AppNavigator

// navigation/AppNavigator.tsx

const Stack = createNativeStackNavigator<AppStackParamList>();

const AppNavigator = () => {
  return (
    <Stack.Navigator screenOptions={{ headerShown: false }}>
      <Stack.Screen name="BottomTabs" component={BottomTabNavigator} />
      {/* Detail screens added here will cover the tab bar completely */}
      <Stack.Screen name="Details" component={DetailsScreen} />
    </Stack.Navigator>
  );
};
Enter fullscreen mode Exit fullscreen mode

💡 Key insight: BottomTabNavigator is just a screen inside AppStack.
Any screen added above it (like DetailsScreen) covers the tab bar entirely.
This is the pattern that controls whether the tab bar is visible.


The Auth Switch (This Was the Most Confusing Part)

4. RootNavigator

// navigation/index.tsx

const RootNavigator = () => {
  const { isAuthenticated } = useAuthStore();

  return (
    <NavigationContainer>
      {isAuthenticated ? <AppNavigator /> : <AuthNavigator />}
    </NavigationContainer>
  );
};
Enter fullscreen mode Exit fullscreen mode

That's it.

No navigation.navigate('Home') after login. No manually resetting the navigation
stack. When isAuthenticated flips to true, React re-renders and the entire
navigator switches automatically.

Same for logout — flip it to false anywhere in the app and the user is back at the
login screen instantly.

This pattern alone would have saved me days of debugging across multiple projects.


Common Mistakes to Avoid

Putting everything in one file.
It works until the project grows. Split it from day one.

Navigating manually after login.
Don't call navigation.navigate('Home') after a successful login. Let state drive
navigation, not imperative calls.

Skipping route types.
Yes, it's 10 minutes of setup. Yes, it saves hours of runtime debugging.

Putting detail screens inside the tab navigator.
They belong in the AppStack above the tabs so the tab bar disappears correctly.


Wrapping Up

Navigation in React Native isn't hard. It just needs the right mental model and a
clear structure before you write the first line.

The full setup comes down to:

  1. One mental model — stacks of cards
  2. Three questions before building anything
  3. One file per navigator
  4. Types first, always
  5. State-driven auth switching — no manual navigation after login

I'm building this as part of my open source React Native starter kit — documenting
every decision as I go. If this was useful, follow along.


Found this useful? Drop a ❤️ or share it with someone stuck on React Navigation.

Top comments (0)