In a React Navigation 7 stack, how do navigate, push, replace and popTo differ, and why did navigate stop going back?
answer
- v7 took the go-back out of navigate
- push adds even a duplicate
- replace swaps the current entry
- popTo: back, or swap in place
- getId or the pop option
basics
~20 sIn React Navigation 7, navigate pushes unless that screen is focused, push adds an entry even for a duplicate, replace swaps the current entry, and popTo returns to an existing screen, the go-back that v7 removed from navigate.
solid answer
~50 sIn a React Navigation 7 stack, `navigate(name, params)` updates the params of the focused screen if it already has that name and otherwise pushes a new entry, even when a route with that name sits lower in the stack. `push` adds a new entry on top even when that screen is already in the stack, which is how you open tracking for a second order while keeping the first one under it. `replace` swaps the current entry for the new one, so back skips it, which suits leaving checkout for tracking. `popTo` goes back to the nearest route with that name, and if there is none it removes the current screen and adds the target in its place. v6's `navigate` sometimes pushed and sometimes popped depending on the stack, which confused people, so v7 split the go-back case into `popTo`; `getId` or `navigate`'s `{ pop: true }` option restore targeted reuse.
code
tsx · 40 linesimport type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { Button, View } from 'react-native';
type RootStackParamList = {
Orders: undefined;
Checkout: undefined;
Tracking: { orderId: string };
CourierChat: { orderId: string };
};
export function CheckoutScreen({
navigation,
}: NativeStackScreenProps<RootStackParamList, 'Checkout'>) {
return (
<Button
title="Pay"
onPress={() => navigation.replace('Tracking', { orderId: 'A123' })}
/>
);
}
export function CourierChatScreen({
navigation,
route,
}: NativeStackScreenProps<RootStackParamList, 'CourierChat'>) {
return (
<View>
<Button
title="Back to tracking"
onPress={() =>
navigation.popTo('Tracking', { orderId: route.params.orderId })
}
/>
<Button
title="Track my other order"
onPress={() => navigation.push('Tracking', { orderId: 'B456' })}
/>
</View>
);
}go deeper
Recall the one-line meaning of each call: navigate opens or updates a screen, push adds another entry, replace swaps the current one, popTo goes back to a named one.
Explain the v7 change: navigate no longer pops back, the focused-same-name case updates params, and popTo falls back to replacing the current screen when the target is missing.
Diagnose duplicate screens after a v6 upgrade and pick the fix per call site, using getId for one-screen-per-entity flows and replace or popTo where history must not grow.
Treat navigation history as product behaviour: agree per flow what back should do, and encode it with the explicit call rather than relying on whichever screen happens to be in the stack.
## The four ways to move in a stack A **stack navigator** in React Navigation 7 (`createNativeStackNavigator` or the JavaScript `createStackNavigator`) keeps an ordered list of routes; the last one is focused and the back gesture removes it. Four methods change that list, and interviewers ask for the difference because each one leaves a different history behind. - **`navigate(name, params, options)`** exists on every navigator. In a stack it targets the focused screen if it has the same name, and pushes otherwise. - **`push(name, params)`** is stack-only and adds a new route on top, even if a route with that name is already in the stack. - **`replace(name, params)`** is stack-only and swaps the current route for a new one; the replaced screen unmounts and back goes to whatever was below it. - **`popTo(name, params, options)`** is stack-only and returns to the nearest earlier route with that name, removing everything above it. ## What each method does to the history Assume a delivery app whose stack is `Orders → Tracking(A123) → CourierChat`, and no `getId` is configured. | Call from the focused screen | Target focused already | Target lower in the stack | Target not in the stack | |---|---|---|---| | `navigate('Tracking', …)` | updates its params, no new entry | pushes a second `Tracking` | pushes `Tracking` | | `push('Tracking', …)` | pushes a second `Tracking` | pushes a second `Tracking` | pushes `Tracking` | | `replace('Tracking', …)` | swaps in a fresh route | swaps the current entry | swaps the current entry | | `popTo('Tracking', …)` | updates its params | pops back to it | removes current, adds `Tracking` | Two cases catch people out: 1. On `Tracking(A123)`, `navigate('Tracking', { orderId: 'B456' })` does **not** open a second screen. It updates the focused route's params to B456, so back leaves tracking entirely. To keep A123 underneath, call `push`. 2. On `CourierChat`, `navigate('Tracking', { orderId: 'A123' })` pushes a **second** tracking screen in v7. To return to the existing one, call `popTo('Tracking', { orderId: 'A123' })`. When `popTo` or `navigate` reaches an existing route, the new params **replace** that route's params by default; the `{ merge: true }` option keeps the old keys and overwrites only the ones passed. Screen `initialParams` are merged underneath in both modes. ## Why v7 changed navigate In React Navigation 6, `navigate('Tracking')` jumped **back** to an existing `Tracking` route if one was in the stack and pushed only if none was. The same line of code could therefore push or pop depending on how the user arrived, which the v7 changelog names as a frequent source of confusion. It was also the documented way to send data back to a previous screen. v7 made the behaviour predictable: - `navigate` never goes back on its own; it stays on the screen if it is focused and pushes otherwise; - `popTo` is the explicit go-back-to-this-screen method; - navigating by an internal route `key` was removed at the same time. Two opt-ins bring targeted reuse back where it is wanted. A screen's **`getId`** prop, for example `getId={({ params }) => params.orderId}`, makes `navigate` look for a route with the same id and focus it instead of adding a duplicate. Passing **`{ pop: true }`** as `navigate`'s third argument makes it pop back to an existing route of that name. For a gradual upgrade, `navigateDeprecated` keeps the v6 behaviour but is itself marked deprecated. ## Choosing the right call - Opening a detail screen from a list: `navigate`. - Opening the same screen again for a different entity while keeping the first in history: `push`. - Leaving a flow the user must not back into, such as payment to tracking: `replace`, or a reset for a longer flow. - Finishing a sub-flow and returning to the screen that started it, often with a result: `popTo`. Only `navigate` works across navigator types; `push`, `replace` and `popTo` belong to stack navigators and are not on the navigation object of a tab or drawer screen. ## Upgrade symptom to recognise A v6 app upgraded to v7 that suddenly accumulates duplicate screens, and needs several back presses to leave them, is almost always relying on the old go-back behaviour of `navigate`. The fix is per call site and by intent: `popTo` where the code meant go back, `getId` where it meant one screen per entity, and `push` where a duplicate is actually wanted.
- How can navigate reuse the tracking screen for the same order instead of stacking duplicates?Give the screen a `getId`, for example `getId={({ params }) => params.orderId}`. `navigate('Tracking', { orderId })` then looks for a route with that id and focuses it rather than adding a second one, while a different order id still gets its own entry. Alternatively pass `{ pop: true }` as navigate's third argument to pop back to an existing `Tracking` route.
- After upgrading from v6 to v7, users need several back presses to leave tracking; what changed?Code that called `navigate('Tracking')` expecting a jump back to the existing screen now pushes a new copy each time. Fix each call site by intent: `popTo` where it meant go back, `getId` where it meant one screen per order. `navigateDeprecated` restores the v6 behaviour temporarily, but it is deprecated itself.
- What happens to the screen that replace removes?Its route leaves the stack state, so its component unmounts and the back gesture goes to the route below it. That is why checkout-to-tracking uses `replace`: the user cannot swipe back into a payment form that has already been submitted.
A stack is a pile of paper forms on a desk: push lays a fresh form on top even if an identical one is already in the pile, replace swaps the top form, and popTo lifts forms off until the one you name is on top.
saying these in an interview costs you the question
- In React Navigation 7, navigate still returns to a screen lower in the stack.
- push is only an alias of navigate in a stack navigator.
- replace keeps the replaced screen mounted underneath the new one.
- popTo throws an error when the named screen is not in the stack.
- push, replace and popTo are available on tab and drawer screens too.