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.
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.
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.
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',
},
},
};
<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."
4. Testing Links
You don't need a real domain or App/Universal Links entitlements (that comes in Week 15) to test the mapping — a custom URL scheme, opened from the terminal or another app, is enough during development.
npx uri-scheme open myapp://post/42 --ios
npx uri-scheme open myapp://post/42 --android
import * as Linking from 'expo-linking';
<Pressable onPress={() => Linking.openURL('myapp://post/42')}>
<Text>Open post 42</Text>
</Pressable>
Note that uri-scheme needs a native dev build (or the classic Expo Go
scheme) — it won't hot-swap into a build that's already running without a reload,
so keep Metro's terminal visible while you test.
5. Hands-on Exercise
Build an auth-gated, deep-linkable app
Combine everything above into one small app with a real login flow and a working link.
Requirements:
- A fake
SignInScreenwith an email field and a "Sign In" button that sets anisSignedInflag in state (a real, persisted session comes in Week 9). - A
MainTabsnavigator with at least two tabs, each with its own nested stack, that only renders onceisSignedInis true. - A
PostDetailscreen, nested under one of the tabs' stacks, that reads apostIdparam. - A
linkingconfig mappingmyapp://post/:postIdto that screen, tested withnpx uri-scheme open. - Confirm that opening the link while signed out lands you on
SignInfirst (React Navigation'sfallback+ your auth check should handle this) rather than crashing.
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?
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?
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?
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?
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.