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.

5 min read

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:

javascriptjavascript
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:

texttext
/about

The 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.

History stack with pushState and Back button

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.

javascriptjavascript
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:

javascriptjavascript
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.

javascriptjavascript
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.

javascriptjavascript
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.

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.

javascriptjavascript
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.

Quick reference

OperationCode
Add history entryhistory.pushState(state, "", "/path")
Replace current entryhistory.replaceState(state, "", "/path")
Listen for navigationwindow.addEventListener("popstate", handler)
Read current statehistory.state
Go backhistory.back()
Go forwardhistory.forward()
Go by offsethistory.go(-2)
URL constraintMust be same-origin
Rune AI

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.
RunePowered by Rune AI

Frequently Asked Questions

Does pushState trigger a page reload?

No. pushState changes the URL in the address bar and adds an entry to the browser history stack, but it does not cause the browser to navigate or the page to reload. This is exactly what makes it useful for single-page apps.

What is the difference between pushState and replaceState?

pushState adds a new entry to the history stack, so the user can click Back to return. replaceState modifies the current entry without adding a new one, so the Back button skips over the replaced URL.

Can I use the History API with any URL?

No. The History API only works with same-origin URLs. You cannot push or replace a URL from a different domain, protocol, or port.

When should I use the popstate event?

Listen for popstate when the user clicks the browser's Back or Forward buttons. This is how single-page apps detect navigation and render the correct view based on the new URL.

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.