---
title: 'History and Location'
source: 'https://academia.sh/en/courses/browser-platform/history-and-location'
course: 'The Browser and the Web Platform'
language: en
updated: '2026-08-17T18:09:10+00:00'
license: 'CC BY-SA 4.0'
---

# History and Location

Writing page state into the address and managing the history stack programmatically; the address's parts, which state belongs to the path, the query, or the fragment, the distinction between pushing and replacing, the navigation event, and the server side's share.

The previous lesson saved state but did not touch state's
relationship to the address. When the filter criterion gets kept in
storage, it comes back once the page reopens; but that state has no
address. The user cannot share the filtered list, cannot bookmark it,
cannot return to the previous filter with the back button.

These are problems of **navigation**, not storage. The browser's
address bar and history stack can get changed without the page
reloading. This lesson establishes the address's parts, how the stack
gets managed, and what client-side navigation requires from the
server.

## The Address's Parts

The Links lesson in the Web Fundamentals and HTML curriculum
introduced the address as a string. On the program side, the address
is an object split into its parts, and each part can get read and
written separately.

```js
// address.mjs — the address's parts and editing the query string
const address = new URL("https://example.test/station/north-slope/log?measurement=temperature&threshold=-5&page=3#summary");

for (const field of ["protocol", "host", "pathname", "search", "hash"])
  console.log(field.padEnd(9), JSON.stringify(address[field]));

// When the filter changes, only the relevant parameters get written, the rest are preserved.
const next = new URL(address);
next.searchParams.set("measurement", "relative humidity");
next.searchParams.delete("page");            // pagination resets when the filter changes
console.log("next      ", next.pathname + next.search + next.hash);

// More than one value can attach to the same name; reading is two separate operations.
next.searchParams.append("tag", "night");
next.searchParams.append("tag", "snow");
console.log("tag single", JSON.stringify(next.searchParams.get("tag")));
console.log("tag all   ", JSON.stringify(next.searchParams.getAll("tag")));

// A relative address gets resolved against the base address.
console.log("relative  ", new URL("../summary?measurement=temperature", address).href);
```

```
protocol  "https:"
host      "example.test"
pathname  "/station/north-slope/log"
search    "?measurement=temperature&threshold=-5&page=3"
hash      "#summary"
next       /station/north-slope/log?measurement=relative+humidity&threshold=-5#summary
tag single "night"
tag all    ["night","snow"]
relative   https://example.test/station/summary?measurement=temperature
```

Three gains of editing the query string with the provided interface,
rather than concatenating it by hand, show up in the output. Escaping
happens automatically: the value with a space is encoded. Irrelevant
parameters get preserved; the threshold value stayed in place while
the filter changed. Multiple values attached to the same name are
supported, and reading a single value and reading all values are
separate operations.

Deleting the pagination parameter is an example of a decision, not
code: the old page number loses its meaning once the filter changes.
Because the address carries state, dependencies of this kind between
states also get enforced on the address.

## Which State Gets Written to the Address

Each part of the address corresponds to a different question.

**The path** is the resource's identity: which station's log is
being viewed. If the resource changes, the path changes.

**The query string** says which view of the same resource is wanted:
filter, sort, page number. The measurement list's filtered state gets
written here, and this is what makes it shareable.

**The fragment** shows a position within the document and never gets
sent to the server. In-page links and temporary view settings the
server does not need to know about fit here.

There is also state that **should not get written** to the address:
hovering over a button, an animation's progress, an open tooltip. The
test is this: does the user expect to see the same screen when they
open this address in a new tab? If the answer is no, the state does
not enter the address.

There is also a privacy criterion. The address gets written into
logs, can travel to external servers through referrers, and gets
shared by the user; personal data and tokens do not get put in the
address.

## The History Stack

The browser's history is not a stack, but a sequence of entries moved
through forward and backward. The program affects this sequence with
three operations.

```js
// history.mjs — the history stack's three operations and which one produces an event
class History {
  constructor(initial) { this.entries = [initial]; this.index = 0; }
  get current() { return this.entries[this.index]; }
  push(entry) {                       // forward entries get discarded
    this.entries.length = this.index + 1;
    this.entries.push(entry);
    this.index += 1;
    return null;                      // no event gets produced
  }
  replace(entry) {
    this.entries[this.index] = entry;
    return null;                      // no event gets produced
  }
  go(delta) {
    const target = this.index + delta;
    if (target < 0 || target >= this.entries.length) return "out of bounds, no-op";
    this.index = target;
    return this.current.state;        // event gets produced, the state object gets carried
  }
}

const hist = new History({ name: "list", state: { measurement: "all" } });
const log = (op, event) =>
  console.log(
    op.padEnd(20),
    "index:", hist.index,
    "| stack:", hist.entries.map((x) => x.name).join(" "),
    "| event:", event === null ? "none" : JSON.stringify(event),
  );

log("start", null);
log("push(temperature)", hist.push({ name: "temperature", state: { measurement: "temperature" } }));
log("push(humidity)", hist.push({ name: "humidity", state: { measurement: "humidity" } }));
log("replace(humidity*)", hist.replace({ name: "humidity*", state: { measurement: "humidity", page: 2 } }));
log("go(-1)", hist.go(-1));
log("go(-1)", hist.go(-1));
log("go(-1)", hist.go(-1));
log("push(wind)", hist.push({ name: "wind", state: { measurement: "wind" } }));
log("go(+1)", hist.go(1));
```

```
start                index: 0 | stack: list | event: none
push(temperature)    index: 1 | stack: list temperature | event: none
push(humidity)       index: 2 | stack: list temperature humidity | event: none
replace(humidity*)   index: 2 | stack: list temperature humidity* | event: none
go(-1)               index: 1 | stack: list temperature humidity* | event: {"measurement":"temperature"}
go(-1)               index: 0 | stack: list temperature humidity* | event: {"measurement":"all"}
go(-1)               index: 0 | stack: list temperature humidity* | event: "out of bounds, no-op"
push(wind)           index: 1 | stack: list wind | event: none
go(+1)               index: 1 | stack: list wind | event: "out of bounds, no-op"
```

Four rules can get read from this output.

**A push opens a new entry and discards the forward entries.** In the
last rows, the push made after going back deleted the two entries
ahead; the forward button no longer works. This is the same in the
browser's own navigation.

**A replace does not increase the entry count.** A page that pushes
for every character typed into the filter field makes the user's
back button unusable: every press undoes one letter. States the user
would want to return to get pushed; intermediate states get replaced.

**The event only gets produced while traversing.** Pushing and
replacing do not produce an event; the code that does them updates
the interface itself. This is the same design as the local storage
event not getting produced in the writing tab, from the previous
lesson.

**Every entry has its own state object.** The event carries this
object; which view gets built when the page goes back gets read from
it. Because the state object gets serialized to get stored, the
Storage APIs lesson's limit applies here too: no function, node, or
live object, and size restricted by an implementation-dependent
limit. The sound approach puts only small information not derivable
from the address into the state object — a scroll position, an open
panel's ID.

One point of the model diverges from the browser: here, moving past
the boundary is a no-op. In the browser, history
belongs to the tab and also contains entries from before the page;
going past the first entry leaves the site. Code therefore cannot
assume "there is an entry to go back to."

## The Server Side's Share

Client-side navigation changes the address without the page
reloading. This has one condition, and when it gets forgotten, the
error shows up only on a second visit: **the server has to give a
valid response when the user opens that address directly.**

If the filtered list's address is visible in the address bar, the
user can copy it into a new tab, bookmark it, or
arrive via search. In these requests, the browser requests
a new document. If the server does not recognize that path, the
response is not found. The status codes defined in the How the
Internet Works curriculum land directly on the user here.

There are two classes of solution. The server produces a real
document for every path; or it maps defined path patterns to a
single document and the client builds the view. The point to watch in
the second is that genuinely nonexistent addresses still get a
not-found response; a server that gives a successful response to
every address gives search engines and link checkers wrong
information.

Writing to the location object is a separate operation and **always
starts a real navigation**. This navigation has two forms: one pushes
an entry to history, one replaces the current entry; the second gets
used for intermediate addresses the user would get redirected from
again if they went back.

## Two Responsibilities During Navigation

Navigation done without a page reload takes over two jobs the browser
otherwise does on its own.

The first is **scroll position**. In a real navigation, the browser
restores the returned-to page's scroll position. On a page where the
document stays put and only content changes, this can fire at the
wrong time: jumping to the old position before new content settles.
The browser therefore offers a manual scroll-restoration setting; the
page keeps the position in its own state object and applies it once
the content has settled.

The second is **requests that do not get canceled**. When data gets
requested for the new view, the user can move to another link without
waiting. If the old request's response arrives later, it
overwrites the view on screen with stale data. The cancellation
signal defined in the Cancellation and Timeout lesson in the
Asynchronous JavaScript and Runtime curriculum is the solution to
this race: every navigation opens its own signal, and the next
navigation cancels the previous one. An uncanceled request's
response, once it arrives, gets checked for which navigation it
belongs to and gets ignored.

## Summary

- The address is an object split into parts; when the query string
  gets edited with the provided interface, escaping happens
  automatically and irrelevant parameters get preserved.
- The path carries the resource's identity, the query the same
  resource's view, the fragment a position within the document; state
  not expected to get rebuilt when shared does not get written to the
  address.
- Pushing to history discards the forward entries; replacing the
  current entry for intermediate states preserves the user's back
  button.
- The navigation event only gets produced when traversing history;
  code that pushes and replaces updates the interface itself.
- Every history entry has a serializable state object; its size is
  limited, and information derivable from the address does not get
  put there.
- Every address client-side navigation produces has to get a valid
  response from the server when requested directly, and nonexistent
  addresses still have to give a not-found response; restoring
  scroll position and canceling stale requests also become the
  page's responsibility.

## Next Step

The address and history define the view the user **wants**. But how
much of that view shows on screen, how far its container has
widened, and when the tree changed cannot get read from this
information. Loading images only once they near the viewport in a
long measurement list, redrawing a chart once its container narrows,
logging newly added rows to a log — all three require continuous
measurement. Doing this by hand on every frame is both expensive and
forces layout by the measurement itself. The next lesson takes up the
family of observers that answer these three questions in a batched,
asynchronous way.
