JavaScript History API Guide: Complete Tutorial
Learn how to navigate, push, and replace browser history entries with the History API. Build smoother single-page navigation without full page reloads.
The History API lets JavaScript control the browser's session history without triggering full page reloads. Instead of navigating to a new URL and rebuilding the entire page, you can update the address bar and manage history entries yourself. This is the core primitive behind every single-page application router.
Here is the simplest history manipulation you can do. Calling pushState updates the address bar without any page reload:
history.pushState({ page: "about" }, "", "/about");
console.log(location.pathname);After running this code, the URL in the address bar changed to the new path and the console confirms it by printing the same value back:
/aboutThe URL in the address bar changes to /about, but the browser did not navigate there. The page stays exactly as it was. The history stack now has a new entry, so the Back button returns to the previous URL. The first argument is a state object you can attach to this history entry for later retrieval.
How the History API works
The browser maintains a stack of visited URLs for each tab. Normally, clicking links or typing URLs pushes entries onto this stack and triggers a full navigation. The History API gives you programmatic control over this stack without the navigation side effect.
Each pushState call adds a new entry on top of the stack. Clicking the Back button pops the top entry and restores the previous URL and state. The replaceState call swaps the current entry without changing the stack size, so the Back button skips to whatever was before it.
pushState: adding history entries
The pushState method takes three arguments: a state object, a title (typically unused, pass an empty string), and a URL path.
history.pushState({ id: 42, section: "settings" }, "", "/settings");The state object can hold any serializable data you need when the user returns to this entry. It is recovered later when the popstate event fires. The URL must be same-origin and relative or absolute on the same domain.
A practical use is updating the URL when the user navigates between views in a single-page app:
function navigateTo(view) {
history.pushState({ view }, "", `/${view}`);
renderView(view);
}
document.querySelectorAll("[data-view]").forEach((link) => {
link.addEventListener("click", (event) => {
event.preventDefault();
navigateTo(link.dataset.view);
});
});Clicking a link calls pushState to update the URL, then renders the new view without a page reload. The browser Back button now works correctly because each view change created a history entry.
replaceState: modifying the current entry
replaceState works exactly like pushState but does not add a new entry. It replaces the state and URL of the current history entry.
history.replaceState({ corrected: true }, "", "/fixed-url");Use replaceState when you want to update the URL for the current view without polluting the history stack. Common use cases include correcting a URL after form submission, removing query parameters after processing them, and updating the URL to reflect the current filter state without creating endless Back button entries.
Listening for navigation with popstate
The popstate event fires when the user clicks Back or Forward, or when JavaScript calls history.back, history.forward, or history.go. This is where you respond to navigation and update your page.
window.addEventListener("popstate", (event) => {
const state = event.state;
if (state && state.view) {
renderView(state.view);
} else {
renderView("home");
}
});The event.state property contains the data object you passed to pushState for that history entry. If the entry was created by normal browser navigation rather than pushState, event.state is null. Always handle the null case with a sensible default.
Navigating programmatically
The History API includes three methods for navigation without pushState. The back method goes to the previous entry in the history stack, exactly like clicking the browser's Back button. The forward method goes to the next entry. The go method accepts a numeric offset.
history.back();
history.forward();
history.go(-2);These methods fire the popstate event just like manual navigation. Use them sparingly. In most cases, letting the user control navigation with the browser buttons is a better experience.
Common mistakes
- Calling pushState with a cross-origin URL. The browser throws a DOMException and the URL does not change.
- Expecting pushState or replaceState to fire popstate. They do not. Only user-initiated navigation and history.back/forward/go trigger popstate.
- Relying on the title argument. Most browsers ignore it. Pass an empty string and update document.title separately if needed.
- Storing non-serializable data in the state object. Functions, DOM nodes, and class instances cannot be cloned via the structured clone algorithm.
- Forgetting to handle popstate on page load. Some browsers fire popstate on load with a null state.
Related articles
- Creating an SPA Router with the JS History API -- building a complete client-side router on top of the History API
- How to Add Event Listeners in JS: Complete Guide -- event handling patterns for popstate
- Handling Click Events in JavaScript: Full Guide -- intercepting link clicks for SPA navigation
Quick reference
| Operation | Code |
|---|---|
| Add history entry | history.pushState(state, "", "/path") |
| Replace current entry | history.replaceState(state, "", "/path") |
| Listen for navigation | window.addEventListener("popstate", handler) |
| Read current state | history.state |
| Go back | history.back() |
| Go forward | history.forward() |
| Go by offset | history.go(-2) |
| URL constraint | Must be same-origin |
Rune AI
Key Insights
- pushState adds a new history entry and changes the URL without reloading the page.
- replaceState modifies the current history entry without adding a new one.
- The popstate event fires when the user navigates with Back or Forward.
- History API URLs must be same-origin. Cross-origin URLs are rejected.
- The state object passed to pushState is recovered in the popstate event.
Frequently Asked Questions
Does pushState trigger a page reload?
What is the difference between pushState and replaceState?
Can I use the History API with any URL?
When should I use the popstate event?
Conclusion
The History API is the foundation of client-side routing in single-page applications. pushState changes the URL without a reload. replaceState updates the current entry. The popstate event lets you respond to Back and Forward navigation. Understanding these three pieces is the first step toward building your own SPA router.
More in this topic
Using Decorators for Logging in JS Architecture
Learn how JavaScript decorators wrap class methods to add logging without touching the original code, including setup with Babel and how execution order works.
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.