The HTML5 History API


This is part 2 in a series on multi-page behaviours in modern web apps

A part of the HTML5 specification introduced in 2011, known as the History API, has transformed the ability of web applications that “run in the browser” to support multi-page behaviours.

The History API allows client-side scripts to programmatically modify the entire URL path—not just the URL fragment, but the whole path—while suppressing the page reload that would normally occur when the URL changes. As its name suggests, the History API integrates with the browser’s history. This allows Client-Side Rendered applications make the browser’s history work as expected when they change the URL, including use of the Back and Forward buttons.

The History API is pretty awesome, but it doesn’t provide support for all the multi-page behaviours—at least, not on its own. In order to illustrate this, I’ve built a simple demo application. It’s inspired by a demo from well known tutorial, but with a few important differences.

Note: When you try the demo application, certain actions result in HTTP 404 Not Found errors. This is intentional. I want to show the limitations of the History API when used on its own, as well as its capabilities. In the next post I’ll describe how to fix these errors—but before that, let’s take a closer look at what the History API can do for us when used on its own.

Here’s a screenshot which links to the demo application.

HTML5 History API demo application

The HTML5 History API — demo application

The demo application runs in entirely in the browser, so only static files are served by the web server, and all content is dynamically generated in the browser using Client-Side Rendering (CSR).

The application displays three randomly-coloured blocks, which are hyperlinks to separate logical “pages” within the application. When you click on a link, a new “page” opens—but a page reload does not occur. The URL path changes to reflect the hex value of the colour that was clicked, and client-side code renders the background of the new page in that colour.

I’ve designed the demo application so you can immediately tell when a page reload is occurring:

  • If the three link colours stay the same: The page did not reload, the DOM structure was preserved, and client-side code was able to retain arbitrary data structures in memory.
  • If the three link colours randomize: A full page reload occurred, the entire DOM structure was rebuilt scratch, and any data structures held in memory by client-side code were discarded and had to be reconstructed.

If you try the demo application, you’ll notice that when you click on a link, the current page changes along with the URL—and yet the three link colours stay the same! This shows that the page is not reloading as you navigate.

I’ll explain how this is achieved later. First, let’s see which multi-page behaviours this application supports in its current form, and which it does not.

Evaluation against the multi-page behaviours

How does this application stack up against the multi-page behaviours? I encourage you to try the demo application and see for yourself what works and what doesn’t.

Shareable

Despite the page not reloading when you click on a link, the entire URL path in the browser’s address bar changes.1 This is the magic of the History API, and it means you can “share” your current location using the browser’s native Share button, or by copying the URL in the address bar.

Therefore we can say that the shareable behaviour is supported.

You can’t actually use a shared URL though. That needs a different behaviour: deep linkable.

Deep linkable

What happens if you share or bookmark a deep link, and then try and open it later?

You can try this with any valid URL, but let’s try it with https://egalo.com/hist/27d98d (“Shamrock”).

An HTTP 404 Not Found error appears. I’ll explain why this happens and how to fix it later. For now, let’s just say that the deep linkable behaviour is not supported by default using the History API.

Refreshable

What happens if you open the application home page, navigate to any colour-specific page, and then click the browser’s Reload button?

Again, an HTTP 404 Not Found error appears. As before, I’ll explain why this happens and how to fix it later. For now, let’s just say that the refreshable behaviour is not supported by default.

History traversable

What happens if you click Back or Forward?

You retrace your navigational steps in the application, just as you’d expect. You can also choose arbitrary pages from your browser’s history by long-clicking the Back or Forward buttons (though not by selecting from the main History menu, since that again requires the deep linkable behaviour).

In a real-life application it might take a considerable effort to make history traversal work as expected, especially if there is a lot of complex state to manage. For now, let’s just say that the history traversable behaviour can be made to work using the History API.

If you right-click on one of the three coloured links, all the standard menu options appear: Open Link in New Tab, Open Link in New Window, Copy Link Address, and so on.

Rejoice! Hallelujah! These are genuine, regular, honest-to-God hyperlinks you can right click. They are not those super-annoying fake hyperlinks that can only be primary clicked.2

This is another great thing about the History API: it works with regular hyperlinks—that is, an anchor element with an href attribute specifying a destination URL. And it fails gracefully—if JavaScript isn’t enabled, they behave just like regular hyperlinks.

So we can say that the link operable behaviour is fully supported using the History API.

Actually opening a new page successfully in a new browser tab is a different thing though. That again requires the deep linkable behaviour—which as we’ve seen, is not supported by default.

Multi-tabbable

Try opening the application home page in one browser tab, and then navigate to a colour-specific page. Then in a second browser tab, open the home page again and navigate to a different colour-specific page.

Multiple tabs

Now you have two browser tabs open with the same application. You can navigate independently in each tab and switch between the tabs at will.

As before, you cannot right-click a link and open it in a new browser tab because the deep linkable behaviour is not supported. Further, if this was an authenticated application, then the developer might have to do extra work to ensure the user’s session is active in both tabs.

But the demo application is an unauthenticated application. And it works in multiple tabs. So I’m going to say that the multi-tabbable behaviour is supported by default. It’s up to the developer not to break it.

Summary

Let’s summarize what we’ve found so far by using the HTML5 History API on its own.

Behaviour Is it working?
Shareable
Deep linkable
Refreshable
History traversable
Link operable
Multi-tabbable

What’s happening under the hood?

So what is actually happening under the hood? The History API allows you to attach an event listener to a hyperlink element (or one of its DOM parents), like this:

linkRowElement.addEventListener('click', colourLinkClickHandler, false);

In the demo application there’s one listener attached to a div element which is a parent to all three hyperlinks.

The listener fires when a link is primary-clicked, and calls a JavaScript function you supply (in this case, colourLinkClickHandler). Your web framework might implement this automatically.

The JavaScript function normally does three things:

  • Calls the event’s preventDefault() method to suppress the page reload that would normally occur when a link is clicked.
  • Calls browser API method history.pushState(). This pushes the link’s destination URL into the browser’s history and simultaneously updates the address bar to show the new URL. Note that the browser history works like a stack: you push items on, and pop them off.
  • Runs whatever client-side code is necessary to update the DOM to reflect the new location.

The third bullet is important. If you leave that out, the URL will change but the page content will stay the same.

You don’t have to redraw the whole page, though. You only have to apply the delta—that is, figure out which parts of the DOM need to change and update only them. In more advanced applications, this is the most complex part to get right.

The key thing to understand is that the DOM structure from the previous page is preserved by default, and it’s the application’s responsibility to make changes. Any data structures the code holds in memory are preserved, too.

Another event listener is attached to the browser’s history:

window.addEventListener('popstate', historyHandler)

When the user clicks Back or Forward (or chooses an arbitrary page from their browser history by long-clicking the Back or Forward buttons), the chosen URL is restored to the browser’s address bar, and control is handed off to another function (in this case, historyHandler). This time, the function only needs to update the DOM as appropriate—it doesn’t need to do the first two things mentioned above. Again, a full page reload does not occur.

And that’s it! In its most basic form, that’s how the History API works.

Why are some multi-page behaviours failing?

Of the six multi-page behaviours, four of them work fine in the demo application: shareable, history traversable, link operable, and multi-tabbable. The remaining two fail: deep linkable and refreshable.

The result is that if you perform any of the following actions, you get HTTP 404 Not Found errors:

  • Click Reload (on any page except the home page)
  • Right-click a link and open it in a new browser tab or window
  • Open a previously-shared deep link or bookmark
  • Pick an arbitrary page from the browser’s full History menu
  • Hack the URL directly in the address bar

Why are we getting all these Not found errors?

The reason is that all those actions involve interrogating the web server for pages that were effectively synthesized in the browser. When we used the History API to generate all those URLs in the address bar and browser history, no-one told the web server about them!

From the web server’s point of view, there’s only one web page: the application’s home page. If you ask it about any other pages, then as far as it’s concerned they don’t exist. Hence, Not found.

For this reason, if you use the History API without doing any extra work on your web server, two of the multi-page behaviours remain unsupported: deep linkable and refreshable.

How can we fix it?

How can we fix these errors? The solution seems obvious, right? Just make a separate copy of the application’s index.html file for each valid URL for this application, and place the copies in folder paths corresponding to those URL paths. Easy!

Not so fast! The demo application generates random URLs corresponding to all the colours in the 24-bit colourspace. There are 16,777,216 colours in the 24-bit colourspace. So for this application you’d need to host 16,777,217 index.html files (including the home page).

I don’t want to do that—and I especially don’t want to host 16,777,217 identical copies of the same index.html file!

Surely there’s a better way? Well, there is a better way. And in the next post I’ll describe it.


  1. Some browsers hide the URL path in the address bar by default, but can optionally be configured to show it. For example using the desktop version of Safari, the entire URL can be shown using Preferences… → Advanced → Smart Search Field → Show full website address

  2. Often these display ‘javascript:void(0);’, ‘about:blank#blocked’, a blank string, or some other infuriating nonsense when you right-click them.