The History API
By Flavio Copes
Learn how the History API manages same-document navigation with pushState, replaceState, popstate, back, forward, and serializable state.
The History API lets a page work with the browser’s session history.
It can move back and forward, add a same-document history entry, replace the current entry, and react when the user returns to an entry.
Single-page applications use it to keep the URL, visible content, and Back and Forward buttons in sync.
Access the history
The API is available as window.history, or just history:
console.log(history.length)
history.length is the number of entries in the current tab’s session history. It does not expose the URLs of those entries.
A page cannot inspect another origin’s history.
Move through history
Go to the previous entry:
history.back()
Go to the next entry:
history.forward()
Move by a relative number of entries:
history.go(-2)
history.go(1)
These methods start an asynchronous navigation. They do not return a promise that resolves when navigation finishes.
If the requested entry does not exist, the browser stays on the current entry.
Add an entry with pushState
pushState() adds a new entry and makes it current:
const state = {
view: 'post',
postId: 42,
}
history.pushState(state, '', '/posts/42')
The arguments are:
- a serializable state value
- an unused parameter, which should be an empty string
- an optional URL
The URL can be relative, but it must resolve to the same origin. A cross-origin URL throws a SecurityError.
The browser changes the address bar, but it does not fetch the URL or update the page content. Your code must render the matching view.
Calling pushState() does not fire popstate. It also does not fire hashchange, even when only the URL fragment changes.
Replace the current entry
replaceState() changes the current entry instead of adding another one:
history.replaceState(
{ view: 'search', query: 'canvas' },
'',
'/search?q=canvas',
)
Use it when the current entry should reflect new state without adding another Back-button step.
A common use is adding state to the initial page entry:
history.replaceState(
{
view: 'home',
},
'',
location.href,
)
Now your popstate handler can restore the initial view as well as entries created later.
Keep state small
The state value must be serializable with the structured clone algorithm.
This works:
history.pushState(
{
page: 2,
filters: ['javascript', 'css'],
},
'',
'/posts?page=2',
)
Functions, DOM nodes, and some other values cannot be cloned. Passing one throws a DataCloneError.
Browsers can impose a size limit and may save state to disk. Store a small identifier or the minimum information needed to restore the view. Keep larger data in sessionStorage, localStorage, IndexedDB, or your application data layer.
The state attached to the active entry is available as:
console.log(history.state)
It can be null.
Handle Back and Forward with popstate
The browser fires popstate when navigation activates another history entry in the same document.
Listen on window:
window.addEventListener('popstate', (event) => {
renderView(event.state)
})
The event’s state property contains a copy of the state attached to the entry.
Back, Forward, and history.go() can trigger the event. pushState() and replaceState() do not.
The URL has changed by the time the handler runs. If your work must wait until the browser finishes other event-loop steps for the new state, schedule it with a zero-delay setTimeout().
A small navigation example
Suppose a page contains this link:
<a href="/about" data-route>About</a>
Intercept same-origin route links, render the view, and add an entry:
document.addEventListener('click', (event) => {
const link = event.target.closest('a[data-route]')
if (!link) return
event.preventDefault()
const url = new URL(link.href)
const state = {
view: url.pathname,
}
history.pushState(state, '', url)
renderView(state)
})
Then restore the view during history traversal:
window.addEventListener('popstate', (event) => {
renderView(event.state)
})
A production router must handle more cases: modified clicks, external links, downloads, fragments, errors, focus, page titles, scroll position, and accessibility.
Direct visits still need a server route
The browser can later load a URL created with pushState(). This happens after a refresh, a shared link, or a browser restart.
Your server or static host must serve a valid page for /about and every other public route. Client-side routing cannot repair a server 404 that happens before JavaScript loads.
The URL should also describe content that can be restored. Do not keep essential navigation state only in the state object.
See MDN’s guide to working with the History API, the pushState() reference, and the popstate event.
Related posts about platform: