1. Installing & Setting Up
React Navigation is a set of packages, not one library — the core plus whichever navigators you actually use, plus two peer dependencies almost every setup needs.
npx expo install @react-navigation/native @react-navigation/native-stack
npx expo install react-native-screens react-native-safe-area-context
import { NavigationContainer } from '@react-navigation/native';
export default function App() {
return (
<NavigationContainer>
{/* your navigator goes here */}
</NavigationContainer>
);
}
NavigationContainer manages the navigation tree and integrates with
the device's back button on Android — every navigator in your app needs to live
inside exactly one of these, at the root.
4. Tab & Drawer Navigators
Beyond a stack, React Navigation ships a bottom tab navigator and a drawer navigator — both installed and used the same way as the stack navigator, and both able to sit alongside or nest with one.
npx expo install @react-navigation/bottom-tabs @react-navigation/drawer
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
const Tab = createBottomTabNavigator();
function MainTabs() {
return (
<Tab.Navigator
screenOptions={({ route }) => ({
tabBarIcon: ({ color, size }) => {
const iconName = route.name === 'Feed' ? 'home' : 'person';
return <Ionicons name={iconName} size={size} color={color} />;
},
})}
>
<Tab.Screen name="Feed" component={FeedScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}
A common real-world shape — one stack navigator per tab, so pushing a detail screen from within a tab doesn't hide the tab bar unexpectedly — is genuinely nested navigation, which gets its own deeper treatment next week. For now, a single tab or drawer navigator alongside your stack is enough to see how they're configured.
5. Headers & Transitions
Every screen accepts an options prop (or a shared
screenOptions on the navigator) to control its header and transition
behavior, without leaving React Navigation's declarative API.
<Stack.Screen
name="Detail"
component={DetailScreen}
options={({ route }) => ({
title: `Item #${route.params.itemId}`,
headerStyle: { backgroundColor: '#7c5cff' },
headerTintColor: '#fff',
headerRight: () => (
<Pressable onPress={() => console.log('shared!')}>
<Text style={{ color: '#fff' }}>Share</Text>
</Pressable>
),
})}
/>
<Stack.Screen
name="Detail"
component={DetailScreen}
options={{
animation: 'slide_from_right', // iOS-style push; try 'fade' or 'slide_from_bottom' too
gestureEnabled: true, // swipe-back gesture
}}
/>
options can be a plain object or, as with title above, a
function of {'{'} route, navigation {'}'} — the function form is what
lets a header's title depend on the params the screen was navigated to with.
6. Hands-on Exercise
Build a 3-screen app with typed navigation
A small app combining a stack and a tab, with fully typed params throughout.
Requirements:
- Define a typed
RootStackParamListwith aHomescreen (no params) and aDetailscreen expecting a required{'{'} itemId: number {'}'}param. - Build a
Homescreen listing at least 6 items; tapping one navigates toDetailwith that item's ID. - On
Detail, type the screen's props withNativeStackScreenProps, readitemIdfromroute.paramswith no manual casting, and set the headertitledynamically to include the item ID. - Add a custom
headerRightbutton onDetailthat logs to the console when pressed. - Add a bottom tab navigator with a second tab,
Settings, alongside your stack (nest the stack inside one tab, or keep them as siblings — either is fine at this stage).
If TypeScript isn't catching a typo'd screen name in navigation.navigate(), confirm you passed RootStackParamList as the generic to both createNativeStackNavigator<RootStackParamList>() and each screen's NativeStackScreenProps<RootStackParamList, 'ScreenName'> — missing it on either side silently falls back to untyped navigation.
7. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why does every navigator in a React Native app need to live inside a NavigationContainer?
Why does every navigator in a React Native app need to live inside a NavigationContainer?
NavigationContainer manages the overall navigation tree and state, and integrates with platform behavior like the Android hardware back button. Every navigator (stack, tab, drawer) needs to be nested inside exactly one container at the root of the app for navigation state to be tracked correctly.
Q2
Without a typed RootStackParamList, what happens if you call navigation.navigate('Detial', {'{'} itemId: 42 {'}'}) with a typo'd screen name?
Without a typed RootStackParamList, what happens if you call navigation.navigate('Detial', {'{'} itemId: 42 {'}'}) with a typo'd screen name?
Without types, the call fails silently at runtime — there's no screen named "Detial" so navigation simply does nothing useful, with no compile-time warning. With a typed param list applied to the navigator and every screen's props, that same typo becomes a TypeScript error before the app ever runs.
Q3
What's the difference between passing an object vs. a function to a screen's options prop?
What's the difference between passing an object vs. a function to a screen's options prop?
A plain object sets fixed header/transition options that never change. A function receives {'{'} route, navigation {'}'} and can compute options dynamically — most commonly, setting a header's title based on the current screen's route.params, which a static object has no way to express.
Q4
Why does createNativeStackNavigator tend to feel more "native" than a custom hand-rolled screen transition?
Why does createNativeStackNavigator tend to feel more "native" than a custom hand-rolled screen transition?
It's built on top of the platform's actual native navigation primitives (UINavigationController on iOS, Fragment-based navigation on Android) rather than reimplementing transitions in JavaScript. That's what gives it authentic platform-specific gestures, transitions, and performance out of the box, matching what a fully native app would do.