From History API to Navigation API: Mastering SPA Route Guards

This article explores the limitations of using History API for SPA route guards, presents two common workarounds, and introduces Chrome's Navigation API as a more centralized solution, detailing its events, transition handling, entry management, and current compatibility concerns.

ELab Team
ELab Team
ELab Team
From History API to Navigation API: Mastering SPA Route Guards

Route Guard

Route guards are mechanisms that execute custom logic at various moments before, during, or after a navigation change in a page.

SPA & History API

In single‑page applications (SPA), route guards are crucial. The common implementation relies on the History API, using window.history.pushState and window.history.replaceState to modify the URL, and listening to window.onpopstate or window.onhashchange for navigation events.

Undetectable pushState & replaceState

The History API cannot directly detect calls to pushState or replaceState; it only detects forward/back navigation. Many applications need to invoke these methods and trigger corresponding UI updates.

Solution One

Register custom listeners and wrap pushState and replaceState with your own functions that first perform the native call and then notify the listeners. React‑Router follows this pattern (illustrated in the diagram below).

This approach requires every module to use the wrapped methods, which limits flexibility when third‑party code calls the native APIs directly.

Solution Two

Overwrite window.history.pushState and window.history.replaceState globally, providing a centralized solution. Example implementation:

const rewrite = function(type) {
  const hapi = history[type];
  return function() {
    // custom logic can be added here
    const res = hapi.apply(this, arguments);
    const eventArguments = createPopStateEvent(window.history.state, type);
    window.dispatchEvent(eventArguments);
    return res;
  };
};

history.pushState = rewrite("pushState");
history.replaceState = rewrite("replaceState");

Garfish adopts a similar technique (see diagram below).

Overriding global methods can cause side effects, such as duplicate listener execution when other modules also dispatch popstate events.

Navigation API Emerges

Chrome’s Navigation API offers a native, centralized way to handle navigation in modern front‑ends. The MDN documentation describes it as the “modern front‑end native routing” and notes its capability to rebuild SPAs.

NavigateEvent

The core of the Navigation API is the navigate event. Example usage:

navigation.addEventListener('navigate', navigateEvent => {
  switch (navigateEvent.destination.url) {
    case 'https://example.com/':
      navigateEvent.transitionWhile(loadIndexPage());
      break;
    case 'https://example.com/cats':
      navigateEvent.transitionWhile(loadCatsPage());
      break;
  }
});

Why a NavigateEvent?

Unlike the History API, the Navigation API’s navigate event can detect both pushState and replaceState calls automatically, providing a more universal and centralized approach.

Transition

The transitionWhile() method accepts a Promise that represents the work needed before the new page becomes active. The browser waits for the promise to settle before completing the navigation.

navigation.addEventListener('navigate', navigateEvent => {
  if (isCatsUrl(navigateEvent.destination.url)) {
    const processNavigation = async () => {
      const request = await fetch('/cat-memes.json');
      const json = await request.json();
      // TODO: handle cat memes JSON
    };
    navigateEvent.transitionWhile(processNavigation());
  } else {
    // load some other page
  }
});

Transition Success and Failure

If the promise resolves (or if transitionWhile is not called), a navigatesuccess event fires:

navigation.addEventListener('navigatesuccess', event => {
  loadingIndicator.hidden = true;
});

If the promise rejects, a navigateerror event fires:

navigation.addEventListener('navigateerror', event => {
  loadingIndicator.hidden = true; // also hide indicator
  showMessage(`Failed to load page: ${event.message}`);
});

Abort Signals for Navigation Cancellation

The event object provides a signal (an AbortSignal) that can be passed to fetch to abort ongoing requests when a navigation is superseded.

navigation.addEventListener('navigate', navigateEvent => {
  if (isCatsUrl(navigateEvent.destination.url)) {
    const processNavigation = async () => {
      const request = await fetch('/cat-memes.json', {
        signal: navigateEvent.signal,
      });
      const json = await request.json();
      // TODO: handle cat memes JSON
    };
    navigateEvent.transitionWhile(processNavigation());
  } else {
    // load some other page
  }
});

Entries

The Navigation API introduces the concept of entries , representing navigation sessions. You can access the current entry via navigation.currentEntry and retrieve the full list with navigation.entries(). The NavigationHistoryEntry interface includes properties such as url, key, id, index, sameDocument, and methods like getState() and an ondispose handler.

interface NavigationHistoryEntry : EventTarget {
  readonly attribute USVString? url;
  readonly attribute DOMString key;
  readonly attribute DOMString id;
  readonly attribute long long index;
  readonly attribute boolean sameDocument;
  any getState();
  attribute EventHandler ondispose;
};

You can read or update the state of an entry with navigation.currentEntry.getState() and navigation.updateCurrentEntry({state: something}).

Navigation Operations

navigation.navigate(url, {state, history: 'auto'|'push'|'replace'}) – equivalent to pushState / replaceState but supports cross‑origin URLs.

navigation.reload({state}) – reloads the current page (similar to location.reload()).

navigation.back() – moves back one entry (like history.back()).

navigation.forward() – moves forward one entry (like history.forward()).

navigation.traverseTo(key) – jumps to a specific entry identified by its unique key, analogous to history.go() but using the entry key.

Limitations

The Navigation API is relatively new and suffers from limited browser support; it is only available in Chrome starting from version 102.

Outlook

The goal of this article is not to exhaustively document every Navigation API method, but to highlight the challenges of using the History API for SPA navigation and to present a more promising alternative. The Navigation API may become the primary approach for SPA routing in the near future, with the History API serving as a fallback.

References

https://developer.chrome.com/docs/web-platform/navigation-api

https://wicg.github.io/navigation-api/

SPAHistory APINavigation APIRoute Guard
ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.