Using the JavaScript History API
Tags: Coding, Programming, Web Development, JavaScript, Software Development

Using the JavaScript History API

Give readers a smoother journey across your JavaScript-enhanced sites

Photo by Kyle Glenn on Unsplash

A short history of history

The back button is the web’s ‘undo’, practically its killer feature second only to the hyperlink. It’s the availability and simplicity of the back button that allows link-following to be a cheap, non-destructive action. The back button encourages readers to browse more.

In pre-JavaScript days, the role of the back button was straightforward: return to the previous page i.e. URL. This was taken literally, but advances in client-side technologies now have us questioning just what a page is and how well it corresponds with a URL.

As an example, consider a page that contains a tabbed interface, in which a set of links display different sections of content. Are these separate ‘pages’? How should browser history, including the back button, cater for navigation between them?

Reevaluating history

It’s clear that we need some extra nuance to handle history when it comes to JavaScript-enhanced pages. Thankfully, the History Web API is here to help. Not only does the History interface allow for programatically navigating through the browser history, it also contains methods to modify that history, including the storage of additional custom state alongside each entry in the history stack.

The replaceState() method allows the current page’s entry in the history to be updated, in response to some other event that may have occurred on the client-side:

history.replaceState(stateObj, title, [url])

Short-term changes

For changes that involve completely transient state, only of interest to a specific instance of navigation, use the history’s stateObj parameter to maintain consistency. In this example taken from my own site, I keep track of which column a table is currently sorted on:

table.addEventListener("click", function(ev) {

var index; if (ev.target.parentNode.parentNode.parentNode.tagName

== 'THEAD'

) {

ev.preventDefault();

index = siblingIndex(ev.target.parentNode);

sortRows(table, index);

history.replaceState({ "sort": index }, "");

}

}

Note that the title parameter is required, even if empty, but the url can be omitted.

On page load, the state is read, and acted on accordingly:

window.addEventListener("load", function(ev) {

if (history.state && history.state.hasOwnProperty("sort")) {

sortRows(table, history.state.sort);

}

});

I use if (history.state.hasOwnProperty("sort")) instead of the simpler if (history.state) to avoid a false negative when storing the 0 index of the first column.

Slightly longer-term

Note that the example above does not make any changes to the URL you’re looking at when you sort the table. This might be fine, but you might also want to modify the URL if you want the new state to exist as a page in its own right.

One potential use for this is incoming links. If you want a reader to be able to sort your table, then either bookmark the resulting view, or link to that specific view from elsewhere, it’s a good idea to provide a unique URL to represent that state. This is the approach I use on my own site; try sorting the table, navigating to one of the game titles, then returning using the back button.

This time, we can move the sort state data from the history’s state object into the URL itself, using URL parameters:

table.addEventListener("click", function(ev) {

var index, url; if (ev.target.parentNode.parentNode.parentNode.tagName

== 'THEAD'

) {

ev.preventDefault();

index = siblingIndex(ev.target.parentNode);

sortRows(table, index);

url = new URL(window.location);

url.searchParams.set("sort", index);

history.replaceState(null, "", url);

}

}

A URL object is created to make search parameter handling much easier. That url is then sent as the optional third argument to replaceState() . Acting on this state is similar to the previous example:

window.addEventListener("load", function(ev) {

var sort;

var url = new URL(window.location);



if (sort = url.searchParams.get("sort")) {

sortRows(table, parseInt(sort));

}

});

The logic is essentially the same (get any existing state, sort the table according to it), but this time there’s an important distinction in how that state is stored: URL parameters are always strings, so we need to parseInt() the column index first.

This time, the URL will change (as a side-effect of the call to replaceState) when we sort the table by different columns. However, note that it won’t create any additional entries in the history stack: we’re replacing the current entry, not adding to the history. In practice, this means you don’t have to keep clicking ‘back’ to return to the previous page, if you’ve clicked several columns to sort by in turn. Only the most recent url is ever stored in the history.

Longer-term and persistent

Alternatives to the History API are available if you want to store this kind of information more permanently, or on a global basis. For example, your site may contain many tables, each of which share a similar structure, and if a reader sorts one table by a specific column, you might want that choice to persist for all.

Longer-term options include localStorage (or even cookies) for client-side handling on static sites, or even full-scale database storage on a back-end server.

Whichever approach you choose, consider what happens when a reader interacts with an element on your page, leaves, and then returns to what they might consider “the same page” later on.

Enjoyed this article? If so, get more similar content by subscribing to Decoded, our YouTube channel!