Move between screens with a navigation stack and decide where each piece of state lives.
Why it matters
A web app has a URL bar and a back button supplied by the browser. A mobile app has neither, so the app itself must keep track of where the user is and how they got there. Getting navigation right is what makes an app feel solid: the back gesture returns where users expect, tabs remember their place, and the details screen shows the item they tapped.
The mental model: a stack of screens
Most mobile navigation is a stack. Opening a screen pushes it on top; going back pops it off. Tabs are several stacks side by side, one per tab.
push("Details", { id: 42 }) pop()
[Home] -> [Home, Details#42] -> [Home]
Each entry in the stack is a route: a screen name plus its params (the data the screen needs, like an id). Navigation state is just data, and the screen you see is a function of that data. That should sound familiar from React: UI = f(state).
Two libraries you will meet
React Navigation is the long-standing library. You declare navigators in code:
import { createNativeStackNavigator } from "@react-navigation/native-stack";
const Stack = createNativeStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}
function HomeScreen({ navigation }) {
return <Button title="Open 42" onPress={() => navigation.navigate("Details", { id: 42 })} />;
}
function DetailsScreen({ route }) {
return <Text>Item {route.params.id}</Text>;
}
Expo Router, used by the default Expo template, is built on React Navigation but uses files as routes, like Next.js. A file app/details/[id].tsx is a screen; you navigate with router.push("/details/42") or a <Link href="/details/42">, and read the param with useLocalSearchParams(). Layout files (_layout.tsx) declare whether a folder is a stack or tabs.
Both give you native transitions and the platform's back behaviour (the Android back button, the iOS swipe from the edge) for free.
Screen state versus app state
Navigation raises the question every React app faces: where does state live?
- Local screen state (
useStateinside the screen) is for things only that screen cares about, like the text in a search box. It is lost when the screen is popped. - Params pass small, serializable values between screens: ids, not whole objects. Pass
{ id: 42 }and let the details screen look the item up, so it never shows stale data. - Shared app state (lifted state, context or a reducer at the top) holds things several screens need, such as the signed-in user or a cart.
A reducer is a good fit for shared state because every change is an explicit action you can test without a phone:
function cartReducer(state, action) {
switch (action.type) {
case "add":
return { ...state, items: [...state.items, action.item] };
case "clear":
return { ...state, items: [] };
default:
return state;
}
}
Worked example: a navigation reducer
Navigation libraries are, underneath, reducers over a stack. Writing a tiny one yourself shows what push, pop, replace and reset really do:
pushadds a route on top.popremoves the top route, but never the last one (there must always be a screen).replaceswaps the top route, which is what you want after a login screen so that back does not return to it.resetreplaces the whole stack, for example after sign-out.
Each returns a new state object and leaves the old one untouched, just like any React reducer.
Common mistakes
- Passing whole objects as params. They can go stale and must be serializable for deep links and state restoration. Pass ids.
- Pushing the login screen on top of the app. After a successful sign-in,
replaceorresetso the back gesture does not return to the login form. - Duplicated screens. Tapping a button twice quickly can push two copies; disable the button or use
navigate, which reuses an existing route with the same name in React Navigation. - Mutating state.
state.stack.push(...)changes the old state in place and React may not re-render. Return new arrays.
Quick recap
- Mobile navigation is a stack of routes (screen name + params); tabs are stacks side by side.
- React Navigation declares navigators in code; Expo Router derives them from files.
- Keep local state local, pass ids as params, and put shared state in a reducer or context.
Try it
In the Do tab you will write navReducer, a small stack navigator as a pure function, plus a helper that reads the current screen. It runs in your browser with no phone needed.