LLMS

Navigation APIの全体像とSPAルーティングのこれから

Baseline 2026で追加されたNavigation APIの主要な機能を紹介します。navigateイベントによるナビゲーションの一元管理、intercept()による宣言的なSPAルーティング、履歴エントリの操作など、SPAに必要な機能が統合的に提供されています。

公開: 2026年3月3日(火)
更新: 2026年3月3日(火)

はじめに

SPAでクライアントサイドルーティングを実現するために、これまではHistory API(pushState/popstate)を使うのが一般的でした。 しかしHistory APIはSPAが普及する前に設計されたものであり、ルーティングライブラリは多くの課題を回避しながら実装する必要がありました。

以下は説明のために簡略化したコードですが、課題の構造を示しています。

// リンクのクリックは個別にハンドリング
document.querySelectorAll('a').forEach((link) => {
  link.addEventListener('click', (e) => {
    e.preventDefault();
    history.pushState(null, '', link.href);
    renderPage(link.href);
  });
});

// ブラウザの戻る/進むは別のイベントで検知
window.addEventListener('popstate', () => {
  renderPage(location.href);
});

リンクのクリックはclickイベント、ブラウザの戻る/進むはpopstateイベントと、ナビゲーションの種類ごとに異なるリスナーを登録する必要があります。 さらにpushState()を呼んでもpopstateは発火しないため、JavaScriptからの遷移を検知するには別途対応が必要です。フォーム送信も考慮すると、ナビゲーション処理はアプリケーション全体に散在してしまいます。

また、popstateはナビゲーションが完了した後に発火するため、ナビゲーションを中断したり、遷移前にデータを取得するといった非同期処理を挟むことが困難です。

実際のルーティングライブラリでは、通常の<a>タグの代わりに<Link>コンポーネントを提供することでこの複雑さを隠蔽しています。 たとえばTanStack Routerでは、<Link>コンポーネントのクリックハンドラで修飾キーやボタン種別を判定してpreventDefault()を呼び出し、Historyモジュールではpopstateリスナーの登録に加えてpushState/replaceStateをネイティブの関数ごと上書きして呼び出しを検知しています。

Baseline 2026で追加されたNavigation APIは、これらの課題を根本から解決します。すべてのナビゲーションを単一のnavigateイベントで捕捉でき、intercept()メソッドでSPAルーティングを宣言的に実現できます。

この記事では、Navigation APIの主要な機能を紹介します。

Navigation APIはwindow.navigationオブジェクトを通じてアクセスします。History APIのpushState()history.back()に代わるメソッドとして、以下が用意されています。

  • navigation.navigate(url):指定したURLへのナビゲーションを開始する(history.pushState()に近いが、ナビゲーションフロー全体を伴う)
  • navigation.back()/navigation.forward():履歴を戻る/進む(history.back()/history.forward()に相当)
  • navigation.traverseTo(key):特定の履歴エントリに直接遷移する(history.go()に相当するが、相対オフセットではなくキーで指定する)
  • navigation.reload():現在のページをリロードする
  • navigation.updateCurrentEntry({ state }):ナビゲーションせずに現在のエントリの状態だけを更新する

updateCurrentEntry()を除くこれらのメソッドは、committedfinishedの2つのPromiseを含むオブジェクトを返します。

const { committed, finished } = navigation.navigate('/next');

await committed; // URLが変更された
await finished;  // ナビゲーションに伴う非同期処理が完了した
  • committed:ナビゲーションがコミットされた(URLバーが更新された)タイミングで解決される
  • finished:ナビゲーションに伴う非同期処理がすべて完了したタイミングで解決される

Navigation APIの最大の特徴は、navigateイベントですべての種類のナビゲーションを一元的にハンドリングできることです。 リンクのクリック、フォーム送信、navigation.navigate()の呼び出し、ブラウザの戻る/進む、これらすべてがnavigateイベントとして発火します。

navigation.addEventListener('navigate', (event) => {
  console.log(event.navigationType); // "push", "replace", "reload", "traverse"
  console.log(event.destination.url);
});

NavigateEventには、ナビゲーションの種類を判別するための豊富なプロパティが用意されています。

  • navigationType:ナビゲーションの種類("push""replace""reload""traverse"
  • destination:遷移先の情報を持つオブジェクト。destination.urlで遷移先のURLを取得できる
  • canIntercept:インターセプト可能か(クロスオリジンではfalse
  • hashChange:フラグメントのみの変更か
  • downloadRequest:ダウンロードリクエストか
  • formData:POSTフォーム送信時のFormData(それ以外ではnull
  • signal:別のナビゲーションが開始されたり、ユーザーが停止ボタンを押した場合に中断されるAbortSignalを返す
  • infonavigate()の呼び出し時に渡された一時的なデータ
  • userInitiated:ユーザーが能動的に発生させたナビゲーションか(navigation.navigate()navigation.back()などJavaScriptからの遷移ではfalse
  • sourceElement:ナビゲーションを発生させた要素(クリックされた<a>など。JavaScriptからの遷移ではnull
  • hasUAVisualTransition:ブラウザがこのイベントの前にビジュアルトランジションを実行済みか

これらのプロパティを使えば、SPAとしてインターセプトすべきナビゲーションを判別できます。

navigation.addEventListener('navigate', (event) => {
  // インターセプト不要なナビゲーションをスキップ
  if (!event.canIntercept) return;
  if (event.hashChange) return;
  if (event.downloadRequest !== null) return;

  // URLに対してルート定義をマッチング
  const url = new URL(event.destination.url);
  const matched = matchRoutes(routes, url.pathname);
  if (matched === null) return;

  // マッチしたルートのハンドラを実行
  event.intercept({
    async handler() {
      await loadRouteData(matched, event.signal);
    },
  });
});

intercept()

NavigateEventintercept()メソッドは、ブラウザのデフォルトのナビゲーション(ページの全体リロード)を抑止し、代わりにSPAとしてのナビゲーションを実行します。

event.intercept({
  async handler() {
    // URLは既に更新済み(コミット後に実行される)
    const data = await fetch(event.destination.url, {
      signal: event.signal,
    });
    renderPage(await data.json());
  },
});

handlerは、ナビゲーションがコミットされた後(URLバーが更新された後)に実行されます。 handlerのPromiseが解決するまでの間、ブラウザはネイティブのローディングインジケータを表示し、ユーザーが停止ボタンを押すとevent.signalが中断されます。

また、intercept()はフォーカスリセットやスクロール復元も自動で処理します。これらはオプションで制御できます。

  • focusReset"after-transition"(デフォルト、自動でフォーカスをリセット)/ "manual"
  • scroll"after-transition"(デフォルト、自動でスクロール復元)/ "manual"

ナビゲーションの完了や失敗はnavigatesuccess/navigateerrorイベントで検知できます。

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

navigation.addEventListener('navigateerror', (event) => {
  showErrorMessage(event.message);
});

<Link>コンポーネントが必須ではなくなる

navigateイベントは通常の<a>タグのクリックでも発火します。 そのため、<a>タグにclickイベントのハンドラーを追加する必要がなく、SPA遷移の実現だけであれば「はじめに」で触れた<Link>コンポーネントは必須ではなくなります。 通常の<a>タグを書くだけでクライアントサイドナビゲーションが成立します。

// History APIベースのルーター — <Link>がクリックハンドリングを内部で行う
import { Link } from 'react-router';
<Link to="/about">About</Link>

// Navigation APIベースのルーター — 通常の<a>タグで十分
<a href="/about">About</a>

履歴エントリの管理

History APIではhistory.lengthで履歴の長さは取得できましたが、個々のエントリのURLや状態を参照する手段はありませんでした。 Navigation APIではnavigation.currentEntryで現在のエントリを、navigation.entries()で同一オリジンの全履歴エントリをNavigationHistoryEntryの配列として取得できます。

const entries = navigation.entries();
for (const entry of entries) {
  console.log(entry.url, entry.key, entry.id, entry.index);
}

NavigationHistoryEntryにはidkeyの2つの識別子があります。

  • id:エントリのインスタンスを一意に識別する。history: "replace"で置き換えると新しいidになる
  • key:履歴のスロットを識別する。history: "replace"で置き換えても同じkeyが維持される

keyは先述のnavigation.traverseTo()で使われます。

// アプリのトップページのkeyを記録しておく
const homeKey = navigation.currentEntry.key;

// どこからでもトップページに戻れる
backToHomeButton.onclick = () => {
  navigation.traverseTo(homeKey);
};

状態の管理

navigate()にはstateinfoの2種類のデータを渡せます。

navigation.navigate('/product', {
  state: { scrollPosition: 100 }, // 履歴に永続化される
  info: { animation: 'slide' },  // 今回のナビゲーションでのみ利用可能
});

stateは履歴エントリに永続化され、entry.getState()で取得できます。History APIのhistory.stateと似ていますが、Navigation APIではエントリごとに独立して管理されるため、navigation.entries()の各エントリから個別に取得できます。

infoは今回のナビゲーションでのみ利用できる一時的なデータで、navigateイベントのevent.infoとして受け取れます。 back()/forward()/traverseTo()でもinfoのみ渡すことができます。

updateCurrentEntry()を使えば、ナビゲーションを伴わずに現在のエントリの状態だけを更新できます。 navigateイベントは発火しませんが、代わりにcurrententrychangeイベントが発火します。 アコーディオンの開閉やタブの切り替えなど、UI状態を履歴に保存したい場合に便利です。

navigation.updateCurrentEntry({
  state: {
    ...navigation.currentEntry.getState(),
    detailsOpen: true,
  },
});

おわりに

Navigation APIは、History APIの根本的な設計上の課題を解決し、SPAのルーティングをブラウザネイティブの仕組みで実現するAPIです。 単一のnavigateイベントによるナビゲーションの一元管理、intercept()による宣言的なSPAルーティング、entries()による安全な履歴操作、そしてstate/infoによる柔軟な状態管理まで、SPAに必要な機能が統合的に提供されています。

History APIでは散在していたナビゲーション処理がnavigateイベントに集約され、<Link>コンポーネントのような中間層も必須ではなくなります。この記事を読んだだけでも、SPAのルーティングライブラリを自作するための道筋がすこし見えてきたのではないでしょうか。

実際にNavigation APIを使ったルーターの実装に興味がある方には、React向けに作られたFunStack Routerのコードがおすすめです。 現時点ではルーティングに必要な最小限の機能に絞って実装されており、Navigation APIの各機能がどのようにルーターとして組み立てられているかをコードベースから読み取りやすいです。

0
もくじ

©︎ 2024〜2026 k8o. All Rights Reserved.