Skip to main content

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
note

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.

App.js
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:

  1. NavigationContainer wraps the whole app and manages navigation state. You need exactly one, at the root.
  2. Stack.Navigator groups screens into a stack. initialRouteName decides which screen shows first.
  3. Each Stack.Screen maps a route name to a component. The name is what you navigate to.
  4. Every screen component automatically receives a navigation prop. navigation.navigate("Details") pushes the Details screen, and navigation.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.

App.js
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 })}
/>
caution

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:

App.js
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.

Combining navigators

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.