Navigation
Real apps have more than one screen. There is no browser and no URL bar on a phone, so navigation works differently: screens are pushed onto a stack and popped off it, and tab bars switch between sections. The community standard for all of this is React Navigation.
Installing React Navigation
React Navigation is split into a core package plus one package per navigator type. Install the core and its peer dependencies:
npm install @react-navigation/native
npx expo install react-native-screens react-native-safe-area-context
We use npx expo install instead of npm install for the native packages. It picks the exact versions that are compatible with your Expo SDK, which avoids a whole category of version mismatch errors.
Then install the native stack navigator, which we will use first:
npm install @react-navigation/native-stack
A stack with two screens
A stack navigator works like a deck of cards: navigating to a screen pushes it on top, and the back button pops it off. Let's build a stack with a Home screen and a Details screen.
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { Button, StyleSheet, Text, View } from "react-native";
function HomeScreen({ navigation }) {
return (
<View style={styles.screen}>
<Text style={styles.title}>Home Screen</Text>
<Button
title="Go to Details"
onPress={() => navigation.navigate("Details")}
/>
</View>
);
}
function DetailsScreen({ navigation }) {
return (
<View style={styles.screen}>
<Text style={styles.title}>Details Screen</Text>
<Button title="Go back" onPress={() => navigation.goBack()} />
</View>
);
}
const Stack = createNativeStackNavigator();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, alignItems: "center", justifyContent: "center" },
title: { fontSize: 22, fontWeight: "bold", marginBottom: 16 },
});
The moving parts:
NavigationContainerwraps the whole app and manages navigation state. You need exactly one, at the root.Stack.Navigatorgroups screens into a stack.initialRouteNamedecides which screen shows first.- Each
Stack.Screenmaps a route name to a component. The name is what you navigate to. - Every screen component automatically receives a
navigationprop.navigation.navigate("Details")pushes the Details screen, andnavigation.goBack()pops it.
Run the app and you get a native header, a platform-correct back button, and the real iOS swipe-back gesture — for free.
Passing params between screens
Screens usually need data: which product was tapped, which user to show. Pass an object as the second argument of navigate, and read it from the route prop on the receiving screen.
function HomeScreen({ navigation }) {
return (
<View style={styles.screen}>
<Text style={styles.title}>Home Screen</Text>
<Button
title="View Mango details"
onPress={() =>
navigation.navigate("Details", { itemId: 42, name: "Mango" })
}
/>
</View>
);
}
function DetailsScreen({ route, navigation }) {
const { itemId, name } = route.params;
return (
<View style={styles.screen}>
<Text style={styles.title}>{name}</Text>
<Text>Item ID: {itemId}</Text>
<Button title="Go back" onPress={() => navigation.goBack()} />
</View>
);
}
You can also use the params in the screen options, for example to set the header title dynamically:
<Stack.Screen
name="Details"
component={DetailsScreen}
options={({ route }) => ({ title: route.params.name })}
/>
Params should be small pieces of data like IDs and titles — think of them like a URL query string. Do not pass full objects that live in your state, because the screen will not re-render when that data changes elsewhere. Pass the ID and look the object up in state instead.
A tab navigator
Most apps also have a tab bar at the bottom. That is a different navigator, from its own package:
npm install @react-navigation/bottom-tabs
Here is a complete app with two tabs:
import { NavigationContainer } from "@react-navigation/native";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { StyleSheet, Text, View } from "react-native";
function FeedScreen() {
return (
<View style={styles.screen}>
<Text style={styles.title}>Feed</Text>
</View>
);
}
function ProfileScreen() {
return (
<View style={styles.screen}>
<Text style={styles.title}>Profile</Text>
</View>
);
}
const Tab = createBottomTabNavigator();
export default function App() {
return (
<NavigationContainer>
<Tab.Navigator
screenOptions={{
tabBarActiveTintColor: "#2563eb",
tabBarInactiveTintColor: "#94a3b8",
}}
>
<Tab.Screen name="Feed" component={FeedScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
</NavigationContainer>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, alignItems: "center", justifyContent: "center" },
title: { fontSize: 22, fontWeight: "bold" },
});
The API mirrors the stack: a navigator component and one Tab.Screen per tab. Switching tabs keeps each tab's state alive, so scrolling position and inputs survive when the user comes back.
Real apps nest navigators: a tab navigator where each tab contains its own stack. You do this by passing a component that renders a Stack.Navigator as the component of a Tab.Screen. Get comfortable with the stack and tabs separately first — nesting is just composition of the two.
Conclusion
You installed React Navigation, built a native stack with two screens, passed params between them with navigate and route.params, and added a bottom tab navigator. With core components, state, and navigation under your belt, you can now build multi-screen React Native apps end to end.