Week 5: Nested Navigators & Deep Linking Basics

Real apps rarely use one navigator in isolation — a stack lives inside each tab so pushing a detail screen doesn't hide the tab bar, and a whole separate navigator swaps in once a user is signed in. This week composes those structures, then wires up deep linking so the app can be opened straight to a specific screen from a URL.

Phase 3 of 8 Week 5 of 22 ~3–4 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Nest a stack navigator inside each tab so the tab bar stays visible while pushing screens
  • Switch between an auth stack and a main app stack based on sign-in state
  • Configure a linking object so a URL opens the app directly to a screen, with params
  • Test deep links locally against a dev build with uri-scheme

1. Nesting Navigators

A tab bar that disappears every time you push a detail screen feels wrong on both platforms. The fix is structural: each tab gets its own stack navigator, and the tab navigator only ever sees the stack's root screen.

a stack per tab
const FeedStack = createNativeStackNavigator();
function FeedStackScreen() {
  return (
    <FeedStack.Navigator>
      <FeedStack.Screen name="Feed" component={FeedScreen} />
      <FeedStack.Screen name="PostDetail" component={PostDetailScreen} />
    </FeedStack.Navigator>
  );
}

const Tab = createBottomTabNavigator();
function MainTabs() {
  return (
    <Tab.Navigator>
      <Tab.Screen name="FeedTab" component={FeedStackScreen} options={{ headerShown: false, title: 'Feed' }} />
      <Tab.Screen name="ProfileTab" component={ProfileStackScreen} options={{ headerShown: false, title: 'Profile' }} />
    </Tab.Navigator>
  );
}

Two details matter here. First, headerShown: false on the tab screen — otherwise you get two headers stacked on top of each other, the tab's and the stack's. Second, pushing PostDetail from inside FeedStack only affects that tab's stack; switching tabs and back preserves where you were.

2. Auth Flows: Conditional Navigators

The standard pattern for a login flow isn't a screen you can navigate away from — it's a different navigator entirely, rendered conditionally based on whether the user is signed in. Once state flips, React unmounts one tree and mounts the other; there's nothing to "go back" to.

App.tsx — switching navigators on auth state
function RootNavigator() {
  const { isSignedIn, isLoading } = useAuth();

  if (isLoading) return <SplashScreen />;

  return (
    <NavigationContainer>
      {isSignedIn ? <MainTabs /> : <AuthStack />}
    </NavigationContainer>
  );
}

const Auth = createNativeStackNavigator();
function AuthStack() {
  return (
    <Auth.Navigator screenOptions={{ headerShown: false }}>
      <Auth.Screen name="SignIn" component={SignInScreen} />
      <Auth.Screen name="SignUp" component={SignUpScreen} />
    </Auth.Navigator>
  );
}

A brief loading state while you check for a stored session (covered properly in Week 9's storage lesson) avoids a flash of the sign-in screen for users who are already logged in.

3. Deep Linking Configuration

A linking object, passed to NavigationContainer, maps URL paths onto your navigator's screen names — turning myapp://post/42 into "open PostDetail with {'{'} postId: '42' {'}'}", automatically.

navigation/linking.ts
import type { LinkingOptions } from '@react-navigation/native';

export const linking: LinkingOptions<RootParamList> = {
  prefixes: ['myapp://', 'https://myapp.example.com'],
  config: {
    screens: {
      FeedTab: {
        screens: {
          Feed: 'feed',
          PostDetail: 'post/:postId',
        },
      },
      ProfileTab: 'profile',
    },
  },
};
App.tsx — passing it to the container
<NavigationContainer linking={linking} fallback={<SplashScreen />}>
  <MainTabs />
</NavigationContainer>

The nested screens object mirrors your navigator's actual nesting — PostDetail is nested under FeedTab here because that's where it lives in the component tree above. A mismatch between the two is the single most common reason a deep link "does nothing."

5. Hands-on Exercise

Hands-on

Build an auth-gated, deep-linkable app

Combine everything above into one small app with a real login flow and a working link.

Requirements:

  1. A fake SignInScreen with an email field and a "Sign In" button that sets an isSignedIn flag in state (a real, persisted session comes in Week 9).
  2. A MainTabs navigator with at least two tabs, each with its own nested stack, that only renders once isSignedIn is true.
  3. A PostDetail screen, nested under one of the tabs' stacks, that reads a postId param.
  4. A linking config mapping myapp://post/:postId to that screen, tested with npx uri-scheme open.
  5. Confirm that opening the link while signed out lands you on SignIn first (React Navigation's fallback + your auth check should handle this) rather than crashing.
Hint

If a deep link opens the app but lands on the wrong screen (or nothing changes), double-check that the screens nesting in your linking config exactly mirrors the navigator nesting in your component tree — React Navigation matches the URL against that shape literally.

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Why give each tab its own stack navigator instead of one shared stack under the tab bar?

A shared stack sits above the tab navigator, so pushing any screen replaces the whole tab bar with it. Nesting a stack inside each tab keeps the tab bar visible while that tab's own history grows, and switching tabs preserves each stack's position independently.

Q2

Why is an auth flow modeled as a whole separate navigator rather than a screen the user navigates to?

There's no meaningful "back" from being signed out to being signed in — it's a state, not a place in a history stack. Rendering AuthStack or MainTabs conditionally, based on isSignedIn, unmounts one tree and mounts the other, so there's no stale screen left behind to navigate back into.

Q3

In a linking config, why must the screens object's nesting match the navigator's actual component structure?

React Navigation resolves a URL path by walking the screens object the same way it would walk the rendered navigator tree — so a PostDetail route nested under FeedTab in the config only matches if PostDetail is genuinely nested under that tab's stack in the app. A mismatch means the URL fails to resolve to any screen.

Q4

Why does testing with a custom scheme like myapp:// not require Universal Links or App Links setup yet?

A custom URL scheme is registered locally with the OS by the app itself and works for any app-to-app or terminal-triggered link, with no server-side configuration. Universal Links (iOS) and App Links (Android) additionally make real https:// URLs open the app instead of a browser, which requires hosting a verification file on a real domain — covered in Week 15.