skip to content

A single-page app wants to send the user back to one specific earlier view after a multi-step flow. How do `navigation.entries()`, `navigation.currentEntry` and `navigation.traverseTo()` make that possible, and why is it more reliable than stepping back a fixed number of entries?

level: seniorimportance: should knowfreq 24%

answer

  1. address the entry, not the distance
  2. key identifies the slot
  3. id changes, key survives replacement
  4. entries can be pruned
  5. replace, don't push, when going back

basics

~20 s

Save navigation.currentEntry.key before the flow starts, then call navigation.traverseTo(key) to return to exactly that entry. A fixed number of back steps is unreliable because redirects, replacements and the user's own navigation change how many entries lie in between.

solid answer

~50 s

Before the flow starts you read `navigation.currentEntry.key` and keep it. Each `NavigationHistoryEntry` — from `currentEntry` or from the array `navigation.entries()` returns — carries a `key` that identifies its slot in the list and stays stable even if the entry is replaced or reloaded, plus `url`, `id`, `index`, `sameDocument` and `getState()`. When the flow ends you call `navigation.traverseTo(savedKey)`, which returns an object with `committed` and `finished` promises. Counting entries instead is fragile: a redirect can add an entry, a `replace` can consume one, the user can navigate on their own mid-flow, and a delta that is one too large walks off your entries entirely — possibly back to whatever site they came from. `traverseTo` either lands on that exact entry or rejects, and rejection is a case you can handle by navigating to the URL fresh with `history: "replace"`. Guard with `entries()` or `navigation.canGoBack` first, because entries can be pruned.

code

javascript · 14 lines
javascript
const returnKey = navigation.currentEntry.key;

async function returnToStartOfFlow() {
  const target = navigation.entries().find((entry) => entry.key === returnKey);
  try {
    if (target) {
      await navigation.traverseTo(returnKey).finished;
    } else {
      await navigation.navigate("/checkout", { history: "replace" }).finished;
    }
  } catch (error) {
    console.warn("could not return to the saved entry", error);
  }
}

go deeper

for a junior

Know that navigation.entries() lists the app's own history entries and that each one has a key you can traverse back to with navigation.traverseTo().

for a middle

Explain what key, id and index mean on an entry, and why key is what you persist while id changes when an entry is replaced.

for a senior

Show the production reasoning: deltas drift because of redirects, replacements and the user's own navigation, and a pruned key needs a defined fallback that replaces rather than pushes.

for a principal

Own the return-point as an explicit part of the flow's design — where the key is stored so it survives reloads, what happens when it is gone, and how that is verified rather than assumed.

## The entry list you can finally see `navigation.entries()` returns an array of `NavigationHistoryEntry` objects: the entries for this tab that belong to the current origin and sit contiguously around the current one. It deliberately does not expose the user's wider browsing history — you cannot see where they were before they reached your site, and you cannot see other origins' entries at all. Within that boundary it is a real, inspectable list, which is the thing the older model never gave you. Each entry exposes: - **`url`** — the entry's URL, or `null` when it is not same-origin-visible. - **`key`** — a stable identifier for the *slot* in the list. Replacing the entry at that slot, or reloading it, keeps the same key. This is the value you persist when you want to come back. - **`id`** — a unique identifier for this particular entry *instance*. Replace the entry and the id changes while the key does not. Use `id` to tell "the same slot but different content" apart, for example when caching per-entry scroll positions. - **`index`** — the entry's position in `entries()`, or `-1` if it is no longer in the list. - **`sameDocument`** — whether traversing to it stays in this document. - **`getState()`** — a structured clone of the state stored with the entry. `navigation.currentEntry` is the one you are on right now, and `currententrychange` fires when it changes. ## Traversing by key ```js const returnKey = navigation.currentEntry.key; // ...several steps later await navigation.traverseTo(returnKey).finished; ``` `traverseTo(key)` asks the browser to traverse to the entry with that key. Like `navigation.navigate()`, `back()` and `forward()`, it returns a result object with two promises: `committed`, which settles once the entry and URL have changed, and `finished`, which settles when any interception handler has finished rendering. Both reject if the traversal cannot be performed, so an unhandled rejection is a real risk — always attach a handler or await inside a `try`. ## Why a fixed delta is the wrong tool Counting is arithmetic on a list you do not control. Between saving your position and wanting it back: - a server redirect during the flow may have added an entry you did not plan for; - a step that used `history: "replace"` consumed a slot instead of adding one, so your count is now too big; - the user may have opened a detail view, come back, or used the back button themselves; - your own code may have pushed an entry for a modal or a filter change. Go back one step too many and you leave your entries altogether — in the worst case landing the user on the site they arrived from, which reads to them as "the app threw me out". Go one too few and they are stranded mid-flow. A key does not drift: it either still exists, in which case you land exactly there, or it does not, in which case you find out and can decide. ## Handling the entry that is gone Keys are not permanent. Navigating forward from an earlier point prunes the entries that were ahead; the user can also arrive at your flow through a fresh document where nothing is saved. Treat a missing key as an ordinary path, not an error: ```js const target = navigation.entries().find((e) => e.key === returnKey); if (target) { await navigation.traverseTo(returnKey).finished; } else { await navigation.navigate("/checkout", { history: "replace" }).finished; } ``` Using `history: "replace"` in the fallback matters: the user asked to *go back*, so adding yet another forward entry would leave the back button pointing at the flow they just escaped. ## Where to keep the key An in-memory variable is fine while the document lives, but a reload destroys it. Storing the key in the entry's own state — the state you pass to `navigation.navigate(url, { state })` — survives reloads and traversals for that entry, which is usually what a multi-step flow wants. Do not put it anywhere shared across tabs: keys identify entries in *this* tab's list, and a key from another tab means nothing here. ## The judgment being tested The interviewer is checking whether you have shipped a flow where "go back to where they started" had to be right — checkout, a wizard, a photo viewer opened from a grid. The naive implementation counts steps and works in the demo. The one that survives redirects, replacements and impatient users addresses the entry by identity and has a defined answer for the case where that identity no longer exists.

  • What is the difference between an entry's `key` and its `id`?
    `key` identifies the slot in the entry list and survives replacing or reloading the entry, so it is what you persist to come back later. `id` identifies that particular entry instance and changes whenever the entry is replaced. Use `id` when you need to tell "same position, different content" apart — per-entry caches such as saved scroll offsets.
  • Can you use `navigation.entries()` to see which site the user visited before yours?
    No. The list is limited to entries for the current origin around the current one in this tab; anything cross-origin is not exposed, and `url` is `null` for entries you are not allowed to read. That restriction is deliberate — a readable browsing history would be a privacy leak, so the API only ever shows you your own entries.
  • Why use `history: "replace"` in the fallback when the saved entry has been pruned?
    Because the user's intent was to go backwards. Pushing a new entry would leave the back button pointing at the flow they just left, so pressing it drops them straight back into the wizard. Replacing the current entry gets them to the right screen without growing the list in the wrong direction.

saying these in an interview costs you the question

  • Steps back a fixed number of entries and hopes the count holds
  • Thinks entries() exposes the user's full browsing history
  • Treats id as the stable identifier instead of key
  • Ignores that traverseTo's promises can reject
  • Pushes a new entry when the user asked to go back

context