Web APIsBrowser & DOMMedium

How does the window.history API work?

01

The Short Answer

The window.history API lets you manipulate the browser's session history — the stack of pages the user has visited in the current tab. pushState() adds a new entry without a page reload, replaceState() modifies the current entry, and popstate fires when the user navigates with back/forward buttons. This is the foundation of client-side routing in SPAs (React Router, Next.js, Vue Router) — it lets you change the URL and update the page without a full reload while keeping the back button working correctly.

02

Core Methods

MethodWhat It DoesTriggers Page Load?
history.pushState(state, '', url)Adds new entry to history stackNo
history.replaceState(state, '', url)Replaces current entry (no new entry)No
history.back()Same as clicking browser back buttonDepends (popstate for SPA)
history.forward()Same as clicking browser forward buttonDepends (popstate for SPA)
history.go(n)Go n steps (negative = back, positive = forward)Depends
history.lengthNumber of entries in the session historyN/A (property)
03

pushState and replaceState

pushState is the key method — it changes the URL in the address bar and adds a new history entry without triggering a page navigation. The browser doesn't send a request to the server; your JavaScript handles the 'navigation' by updating the DOM. replaceState does the same but overwrites the current entry instead of adding a new one — useful for redirects or updating state without cluttering the back button history.

push-replace-state.tstypescript
// pushState(stateObject, unused, url)
// - stateObject: data associated with this history entry (retrieved on popstate)
// - unused: historically 'title' — pass empty string
// - url: the new URL to display (must be same origin)

// Navigate to a new 'page' without reload
history.pushState({ page: 'products', id: 42 }, '', '/products/42');
// URL bar now shows /products/42
// No HTTP request — page doesn't reload
// Back button now has an entry to go back to

// Replace current entry (no new back-button entry)
history.replaceState({ page: 'products', id: 42, tab: 'reviews' }, '', '/products/42?tab=reviews');
// URL updates but back button still goes to the PREVIOUS page
// Useful for: filters, sort order, tab changes

// State object is cloned (structured clone algorithm)
// Can contain: objects, arrays, numbers, strings, Dates, Maps, Sets
// Cannot contain: functions, DOM elements, class instances

// Read current state
console.log(history.state); // { page: 'products', id: 42, tab: 'reviews' }

// URL restrictions: must be same origin
// ✅ history.pushState({}, '', '/new-path')
// ✅ history.pushState({}, '', '?query=value')
// ✅ history.pushState({}, '', '#section')
// ❌ history.pushState({}, '', 'https://other-domain.com') // SecurityError
04

The popstate Event

popstate fires when the user navigates through history (back/forward buttons or history.back()/history.forward()). It does NOT fire when you call pushState or replaceState. The event's state property contains the state object you passed to pushState/replaceState for that entry. This is how SPA routers know which page to render when the user clicks back.

popstate.tstypescript
// Listen for back/forward navigation
window.addEventListener('popstate', (event) => {
  // event.state is the state object from pushState/replaceState
  console.log('Navigated to:', event.state);

  if (event.state?.page === 'products') {
    renderProductPage(event.state.id);
  } else if (event.state?.page === 'home') {
    renderHomePage();
  } else {
    // null state — initial page load entry
    renderHomePage();
  }
});

// Simulate navigation
history.pushState({ page: 'home' }, '', '/');
history.pushState({ page: 'products', id: 1 }, '', '/products/1');
history.pushState({ page: 'products', id: 2 }, '', '/products/2');

// User clicks back button:
// popstate fires with state: { page: 'products', id: 1 }
// URL changes to /products/1

// User clicks back again:
// popstate fires with state: { page: 'home' }
// URL changes to /

// ⚠️ popstate does NOT fire on pushState/replaceState
// You must update the UI yourself when calling those methods
05

Building a Simple Router

Here's a minimal client-side router that demonstrates how SPA frameworks use the History API. It intercepts link clicks, uses pushState to update the URL, and renders the appropriate content. The popstate listener handles back/forward navigation. This is essentially what React Router and similar libraries do under the hood (with much more sophistication).

simple-router.tstypescript
type Route = {
  path: string;
  render: () => string;
};

const routes: Route[] = [
  { path: '/', render: () => '<h1>Home</h1>' },
  { path: '/about', render: () => '<h1>About</h1>' },
  { path: '/contact', render: () => '<h1>Contact</h1>' },
];

function navigate(path: string) {
  history.pushState({ path }, '', path);
  renderRoute(path);
}

function renderRoute(path: string) {
  const route = routes.find((r) => r.path === path);
  const app = document.getElementById('app')!;
  app.innerHTML = route ? route.render() : '<h1>404</h1>';
}

// Handle back/forward
window.addEventListener('popstate', (event) => {
  const path = event.state?.path ?? window.location.pathname;
  renderRoute(path);
});

// Intercept link clicks (prevent full page reload)
document.addEventListener('click', (event) => {
  const target = event.target as HTMLElement;
  const anchor = target.closest('a');
  if (anchor && anchor.origin === window.location.origin) {
    event.preventDefault();
    navigate(anchor.pathname);
  }
});

// Initial render
renderRoute(window.location.pathname);
06

Common Patterns

Use pushState for:

  • Page navigations that should be in the back-button history
  • Route changes in SPAs (React Router uses this internally)
  • Pagination — /products?page=2 should be a new history entry
  • Opening detail views — /products/42 from a list

Use replaceState for:

  • Filter/sort changes — don't pollute back history with every filter toggle
  • Redirects — replace the redirect URL with the final destination
  • Updating query params without adding history entries
  • Storing scroll position or form state on the current entry
07

Why Interviewers Ask This

This question tests understanding of how SPAs work under the hood. Interviewers want to see that you know how client-side routing works without page reloads (pushState + popstate), understand the difference between pushState and replaceState (new entry vs replace), know that popstate only fires on back/forward navigation (not on pushState calls), can explain the same-origin restriction, and understand how frameworks like React Router build on this API. It reveals whether you understand the browser platform or just use framework abstractions without knowing what they do.

Quick Revision Cheat Sheet

pushState: Add history entry + change URL — no page reload, back button works

replaceState: Replace current entry — URL changes but no new back-button entry

popstate event: Fires on back/forward — NOT on pushState/replaceState

history.state: Read the state object of the current history entry

Same-origin only: Can only pushState to URLs on the same origin (security)

SPA routing: pushState + popstate + intercepted clicks = client-side router