In Expo Router 57, how do router.navigate, router.push and router.replace differ in what they add to a stack's history?
answer
- one adds, one maybe, one swaps
- push always adds an entry
- navigate updates the same screen in place
- navigate no longer unwinds
- replace keeps history length
basics
~20 spush always adds a stack entry; navigate adds one unless the target is the screen already on top with the same path params, which it updates in place; replace swaps the current entry, keeping history length unchanged.
solid answer
~50 sAll three take an `Href` and are also Link behaviours: `<Link>` navigates by default and accepts `push` or `replace` props. **`router.push`** always adds a history entry, so pushing `/medicine/42` twice needs two back presses. **`router.navigate`** adds an entry too, except when the target is the screen already on top with the same path params, which is updated in place, so tapping a link to the current page does not stack a duplicate. In Expo Router 57 navigate **does not unwind** to an earlier matching screen; older guides still describe that, but the router's own test for it is skipped with a note that navigate now behaves like push, and `router.dismissTo` is the call that goes back. **`router.replace`** swaps the current entry for the target, leaving the history length unchanged, which suits sign-in completion and redirects. In a tab navigator these calls switch tabs instead of stacking screens.
code
tsx · 20 lines// src/app/medicine/[id].tsx
import { Link, router, useLocalSearchParams } from 'expo-router';
import { Pressable, Text, View } from 'react-native';
export default function MedicineScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
return (
<View>
<Text>Medicine {id}</Text>
{/* Each similar item is its own step: push adds an entry every time. */}
<Link href={{ pathname: '/medicine/[id]', params: { id: 'm-204' } }} push>
Similar: m-204
</Link>
{/* Same screen, same params: navigate updates in place, no duplicate. */}
<Pressable onPress={() => router.navigate({ pathname: '/medicine/[id]', params: { id } })}>
<Text>Refresh this page</Text>
</Pressable>
</View>
);
}go deeper
Recall that push always adds a history entry, replace swaps the current one, and Link uses navigate unless given push or replace.
Explain navigate's rule: it adds an entry unless the target is the screen already on top with the same path params, and that it does not unwind in current Expo Router.
Pick calls by their history effect in real flows, using replace after sign-in, push for chains of detail screens and dismissTo to return to an existing screen.
Define a team convention for history semantics so back behaviour stays predictable across flows, and audit code written against the older unwinding navigate.
## Three calls, three effects on history In a stack every visit is a **history entry**, and the back button pops entries. The three core calls from `expo-router` treat that history differently: | Call | Target is a different screen | Target is the screen on top, same path params | History length | |---|---|---|---| | `router.push(href)` | adds an entry | adds another entry | +1 | | `router.navigate(href)` | adds an entry | updates the top entry in place | +1 or unchanged | | `router.replace(href)` | swaps the top entry | swaps the top entry | unchanged | `<Link href>` uses **navigate** by default; its **`push`** prop switches it to push and its **`replace`** prop to replace. ## push: always a new entry `router.push` never reuses or removes entries. Pushing `/medicine/42` from `/medicine/42` stacks a second copy, and Expo Router's tests confirm that pushing the same route twice needs two back presses. That is what you want for "related product" chains, where each screen should be its own step, and what you do not want for a button that may be tapped twice. ## navigate: new entry unless it is already on top `router.navigate` looks at the stack that owns the target: 1. If the target is a **different screen**, or the same screen with **different path params** (`/medicine/43` from `/medicine/42`), it adds a new entry, like push. 2. If the target is the **screen already on top with the same path params**, it updates that entry in place, so a link to the current page does not create a duplicate. What navigate **no longer** does in Expo Router 57 is **unwind**: it does not pop back to an earlier entry with the same name. Some guides still describe the older unwinding behaviour, but in the pinned source the test named "navigate will unwind the stack" is skipped with the note that navigate changed to be like push. When you want to go back to an existing screen, use `router.dismissTo(href)`. ## replace: swap the current entry `router.replace` removes the current entry and puts the target in its place: - after a successful sign-in, so pressing back from the cart does not return to the sign-in form; - for redirects, which is exactly what the `Redirect` component does internally; - for "step done" screens that must not be revisited, such as an order-placed confirmation replacing the payment step. ## Tabs and other navigators The router finds the **deepest navigator** where the current state and the target diverge and acts there. If that navigator is a **tab navigator**, push and navigate simply **switch the selected tab**; there is no stack history to add to. Push is only meaningful in a stack. ## Choosing in the pharmacy app - Product card to product details: **navigate** (Link default); a duplicate tap on the same product does nothing harmful. - "Similar medicines" carousel on a product page: **push**, so each product is a step the user can back out of. - Sign-in finished: **replace** the sign-in screen, or **dismissTo** the cart if the cart is underneath. - Returning to a screen already in the stack: **dismissTo**, not navigate. ## Reading history bugs Most navigation bugs reported by testers are history bugs, and the call that caused them is usually visible from the symptom: - **"Back shows the same product twice"**: a `push` (or Link with `push`) fired twice, or push was used where navigate would have updated in place. - **"Back returns to the sign-in form after signing in"**: the success handler used push or navigate instead of replace or dismissTo. - **"The app opens a second cart instead of going back to it"**: code written for the older, unwinding navigate; switch to `dismissTo('/cart')`. - **"Back leaves the app from a deep-linked screen"**: history is shallow on cold start; that is a layout concern (an initial route for the stack) rather than a choice between these three calls. Asking "what did this call add to history?" turns each of these into a one-line fix. ## Why interviewers ask The question separates candidates who memorised "navigate is smart" from those who know the current behaviour and its history effects. A good answer names the history effect of each call, the same-screen update rule for navigate, the absence of unwinding in current Expo Router, and replace's role in keeping sign-in and redirect screens out of history.
- The cart is two screens below the top; router.navigate('/cart') adds a second cart screen. Which call returns to the existing one?`router.dismissTo('/cart')`. It pops screens until the cart entry is on top; if no cart entry exists in that stack, it replaces the current screen with the cart instead. navigate no longer unwinds in Expo Router 57.
- What do router.push and router.navigate do when the target sits in a tab navigator?They switch the selected tab. The router acts on the deepest navigator where the current and target states diverge; for a tab navigator that means jumping to the tab, because tabs have no stack history to push onto.
saying these in an interview costs you the question
- router.navigate pops back to an existing matching screen in the stack.
- router.push refuses to add the screen already on top.
- router.replace adds an entry and hides the previous one.
- Link always pushes a new entry by default.
- push inside a tab navigator stacks tab screens.