The popstate event allows web applications to respond to browser session history navigation in JavaScript. With deep integration into the history stack, it unlocks more seamless navigations and state management. This comprehensive guide covers all you need to master working with popstate events.
Introduction to the Browser History Stack
Modern browsers maintain a session history of visited pages and states within a tab or window. This enables the back and forward navigation that web users are accustomed to.
Technically, the history stack consists of a list of history entries. Each entry records details like:
- The URL/document loaded
- The document state when it was loaded
- A title for the entry
- (Optional) Any developer-provided state object
By default, an entry is added to the stack every time the page changes, gets reloaded, or the hash (URL fragment) changes.
The browser manages which is the currently active entry as users navigate back and forward.
What is the popstate Event?
The popstate event fires whenever the active history entry changes. This happens when:
- The user clicks back or forward in the browser.
- The current entry is manipulated using the History API methods.
Specifically, it is triggered when the new active entry was created by a call to history.pushState() or history.replaceState().
So in essence, the popstate event allows JavaScript code to handle any changes in the browser history stack caused by user navigation or the History API.
Key Reasons to Use popstate
Here are some of the key use cases where popstate shines:
-
Dynamic page updates: By listening for
popstate, you can update page content based on the state stored in the active history entry, creating seamless navigations. -
State persistence: Store app-specific state objects in history entries so they persist across sessions.
-
Bookmarking: Entries added via
pushState()are bookmarkable for permalinks. The titles can be used for descriptive bookmarks. -
Caching: Browser caching can be leveraged across history entries.
Compared to other events like hashchange or full refreshes, popstate offers better performance and flexibility for many use cases.
A Quick Refresher on Session History and State
Before going further, let‘s recap how session history entries and state works in the browser.
The History API introduced two key methods:
pushState()– Adds a new entry to the history stack.replaceState()– Updates the current entry.
For example:
// Add first entry
history.pushState({page: 1}, "Title 1", "?page=1");
// Add second entry
history.pushState({page: 2}, "Title 2", "?page=2");
// Replace current entry
history.replaceState({page: 3}, "Title 3", "?page=3");
After the above calls, our history stack looks like:
| Entry | URL | Title | State |
|---|---|---|---|
| 1 | ?page=1 | Title 1 | {page: 1} |
| 2 | ?page=2 | Title 2 | {page: 2} |
| Current | ?page=3 | Title 3 | {page: 3} |
The optional state parameters allow associating any custom data with each entry.
Now when the user clicks back and forward, these entries will be navigated between, with their state available in the popstate event.
How to Listen for popstate Events
When a history navigation happens, the popstate event fires on the window object.
There are two main ways to handle this event:
1. Using addEventListener():
window.addEventListener("popstate", function(event) {
// handle event
});
2. Via the onpopstate handler property:
window.onpopstate = function(event) {
// handle event
};
Now within the handler, we can access details like the updated URL and state using the event properties:
function handlePopState(event) {
const url = document.location;
const state = event.state;
// update UI based on url and state
}
window.addEventListener("popstate", handlePopState);
That covers the basics! Next we‘ll explore some example use cases.
Examples of Handling Session Navigation
popstate events enable us to react seamlessly to browser history navigation triggered by the user‘s back/forward clicks or JavaScript APIs.
For example, consider a multi-page app that shows a profile list on /home and individual profiles at urls like /profile/1 and /profile/2.
We can wire up popstate handlers to update the UI as users navigate between these pages:
Wire up Handler
window.addEventListener(‘popstate‘, handlePopState);
function handlePopState(event) {
let view;
switch(document.location) {
case ‘/home‘:
view = showProfilesList();
break;
case ‘/profile/:id‘:
const id = // get id from URL
view = showProfile(id);
break;
default:
view = show404();
}
displayView(view);
}
This dynamically chooses what view to display based on the updated URL in document.location on every history navigation.
Push New Entries
We can also create new browse history entries using pushState():
function loadProfile(id) {
// Fetch profile data
profileData = //...
// Push new history entry
history.pushState(profileData, ‘‘, `/profile/${id}`);
handlePopState(); // Manually handle
}
handlePopState(); // Also called automatically by browser
This approach allows bookmarking capability and native-feeling navigation between states.
Caching and Restoration
By persisting the app state in history entries, we also get caching and restoration for free:
function loadProfiles() {
// Fetch list of profiles
let profiles = //...
history.pushState(profiles, ‘Profile list‘);
}
handlePopState(event) {
// Restore cached profiles
let profiles = event.state;
showProfilesList(profiles);
}
Now when navigating back from an individual profile, the list can be restored instantly from the history state rather than needing to re-fetch.
Considerations for Complex State Objects
For efficiency, pushState() and replaceState() perform a shallow copy of the state object provided. This means only top-level object references get copied:
let state = {
data: {
info: [‘a‘, ‘b‘]
}
};
history.pushState(state, ‘Test‘);
Only state itself is copied, while data and nested arrays continue to reference the original objects.
This is important when modifying objects over time:
// Push entry with data array
let state = {
data: [‘a‘, ‘b‘]
};
history.pushState(state);
// We modify data later
state.data.push(‘c‘);
// Navigating back, entry state STILL contains original data
handlePopState(event) {
console.log(event.state.data); // [‘a‘, ‘b‘, ‘c‘]
}
The changed arrays are reflected in history state objects previously pushed.
To avoid this, we must pass copies of nested state that could change instead of directly referencing mutable objects.
Alternative Patterns Using Hashchange
The hashchange event has a similar use case to respond to URL changes – what about using that instead?
The key difference is hashchange only responds to changes in the URL fragment (after the #). So route changes require using hashes like example.com/#/profile instead of real paths.
popstate has several advantages over this approach:
-
Entries are added to the standard browse history stack instead of invisibly changing hash values. This keeps the expected back/forward navigation.
-
It offers flexibility separate from the URL fragment.
-
Performance can be better as full document reloads are not needed to change system state.
However, hash routes do have benefits like cross-browser compatibility. For newer browsers supporting the History API, popstate is generally recommended as a more native-feeling and flexible approach.
Browser Support
popstate event support is excellent in modern browsers:
| Browser | Version Added |
|---|---|
| Chrome | 5.0 |
| Firefox | 4.0 |
| Safari | 5.0 |
| Edge | 12.0 |
Support is less consistent in older browsers like IE10 and Opera Mini. For older browsers there are history.js polyfills that can help normalize behavior.
Current Usage
The following usage statistics indicate the wide adoption of popstate events today:
| Website | Approx. Usage |
|---|---|
| 450 million visits/month | |
| Youtube | 150 million visits/month |
| Wikipedia | 62 million visits/month |
| 45 million visits/month |
With over 3.7 billion overall page visits per month to sites leveraging these events, popstate has proven itself as a staple in modern web development.
Integrating popstate creates seamless navigations that both developers and users love.
Key Takeaways
Here are some key points to remember about working with browser history and popstate:
- The browser session history is comprised of entries tracking loaded documents, UI state, URLs and more.
popstatefires whenever the active history entry changes through user navigation or JavaScript manipulations.- This event can be handled to update UIs and restore cached state data.
- Methods like
pushState()integrate state data into the history stack. - Used properly, the History and
popstateAPIs enable smooth navigations and state handling.
Conclusion
Having a solid grasp of browser history mechanics and popstate events unlocks the potential for reactive single-page applications. Manipulating session history programatically is very powerful, but does require careful handling to not disrupt standard browser functions.
With robust cross-browser support and proven large-scale production use, popstate is here to stay as a pivotal aspect of modern web development. Understanding how to properly leverage it will soon become an assumed baseline web development skill.


