SPA ルーティングとは - React/Vue/Angular でのページ遷移の仕組み

SPA ルーティングは URL とコンポーネント表示を同期させページ遷移なしで画面を切り替える仕組み。History API / Hash モード / SSR との関係を解説

フロントエンド

SPA ルーティングとは

SPA ルーティングは、Single Page Application でブラウザの URL とコンポーネントの表示を同期させる仕組みである。ページ全体をリロードせずに、URL の変更に応じて表示するコンポーネントを切り替える。History API (pushState, popstate) を使って、ブラウザの戻る/進むボタンにも対応する。

仕組み

SPA ルーティングでは、ブラウザの History API (pushState) を使って URL を変更し、JavaScript がその URL に対応するコンポーネントを描画する。サーバーへのリクエストは発生せず、ページ全体のリロードなしに画面が切り替わる。

従来のマルチページ:
  /about にアクセス → サーバーが about.html を返す → ページ全体をリロード

SPA ルーティング:
  /about にアクセス → JavaScript が URL を検知 → About コンポーネントを表示
  (サーバーへのリクエストなし、ページリロードなし)

ここで押さえておきたいのは pushState() が何をしないかである。pushState() は履歴に項目を積んで表示上の URL を書き換えるだけで、その URL をブラウザが取りに行くことはない (ただしブラウザを再起動した後などに、後から読み込みを試みることはある)。現在の URL と同一オリジンでない URL を渡すと例外になる。さらに、ハッシュだけが変わる呼び出しであっても hashchange は発火しない。

popstate が発火するのは戻る/進むのような履歴移動のときで、pushState() を呼んだ本人には発火しない。つまり「URL を変えたら描画する」処理はルーターが自分で呼ぶ必要がある。この非対称性を知らずに popstate だけで描画を組むと、戻るボタンは効くのにリンククリックでは画面が変わらないという症状になる。

React Router での実装

React Router では BrowserRouter が History API を、HashRouter がハッシュを受け持つ。Linka 要素を描画したうえでクリックを引き取り、ブラウザ既定の画面遷移を止めて履歴 API 経由の切り替えに置き換える。見た目はリンクだがサーバーへのリクエストは飛ばない。path="*" は他のどのルートにも当たらなかったときの受け皿で、これを置き忘れると未知の URL で何も描画されない空白画面になる。

import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/users/123">User</Link>
      </nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/users/:id" element={<UserProfile />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

Hash ルーティング vs History ルーティング

2 つの方式は、画面の状態を URL のどこに持たせるかが違う。ハッシュ (# 以降) はリクエストとしてサーバーへ送られないため、どの画面を開いてもサーバーには同じ 1 本の URL しか届かない。History ルーティングはパス自体が変わるので、そのパスに対する応答をサーバー側で用意しておかないと直リンクが成立しない。

方式URL 例サーバー設定つまずきどころ
Hashexample.com/#/about不要パスがサーバーへ届かないため、サーバーログや配信側の計測に画面単位の記録が残らない
Historyexample.com/about全パスを index.html へ返すフォールバックが必要設定漏れは直リンクとリロードでしか出ないため、SPA 内を回っているだけでは気づけない

# の無い URL を取れる History ルーティングが既定の選択肢になるが、その代償がこのフォールバック設定である。

CloudFront + S3 の構成では、フォールバックはカスタムエラーレスポンスで組む。/about というキーのオブジェクトは存在しないので、S3 はオブジェクトが無いときのエラーを返す。ここで返るコードは権限に依存し、リクエスト元に s3:ListBucket があれば 404 Not Found、無ければ 403 Access Denied になる。バケットを非公開にして CloudFront からのみ読ませる構成では後者になりやすいので、403 と 404 の両方をカスタムエラーレスポンスに登録し、応答ページを /index.html、閲覧者へ返すステータスコードを 200 に上書きしておく。CloudFront が上書き先として指定できるコードは 200 / 400 / 403 / 404 / 405 / 414 / 416 / 500 / 501 / 502 / 503 / 504 である。

この上書きには副作用がある。本当に存在しないパスも 200 と index.html で返るため、外から見れば中身の無いページが正常応答している状態になる。SPA 側で未知のパスを「見つかりません」の画面として描き、その画面ではインデックスを拒否するといった手当てが別途必要だ。切り分けの手掛かりとして、標準ログの sc-status は上書き後の 200 になるが x-edge-result-type にはオリジン側の結果 (Error) が残る。

コード分割との連携

ルート単位のコード分割は SPA ルーティングと組み合わせやすい。lazy で読み込みを遅らせると、そのルートに入るまで対応するチャンクを取得しない。ただし遷移のたびにチャンク取得が挟まるので、初回表示が軽くなる代わりに遷移時の待ちが生まれる。Suspense の代替表示を用意し、よく踏まれるルートは先読みしておくと体感の落ち込みを抑えられる。

const About = lazy(() => import('./pages/About'));
const UserProfile = lazy(() => import('./pages/UserProfile'));

// /about にアクセスした時だけ About のコードを読み込む
<Route path="/about" element={
  <Suspense fallback={<Loading />}>
    <About />
  </Suspense>
} />

SEO の課題

SPA は初期 HTML が空 (<div id="root"></div>) のため、コンテンツは JavaScript の実行後にしか現れない。Google は JavaScript のページをクロール・レンダリング・インデックスの 3 段階で処理すると説明しており、リソースに余裕ができた時点で headless Chromium がページを描画して JavaScript を実行する。実行されないわけではないが、クロールとレンダリングはそれぞれ待ち行列に入り、数秒で済むこともあればそれより長くかかることもある (2026 年 8 月時点)。HTTP ステータスが 200 以外の応答ではレンダリングが省かれることがある点も、フォールバックを 200 で返す理由になる。一方で SNS のリンクプレビューのように JavaScript を実行しない取得元は珍しくないため、初期 HTML に何も無い構成はそこで崩れる。対策:

  • SSR (Next.js): サーバーで HTML を生成
  • SSG: ビルド時に静的 HTML を生成
  • プリレンダリング: クローラー向けに事前レンダリング

どちらの方式を選ぶか

配信側の設定を触れる構成なら History ルーティングでよい。ハッシュを選ぶ理由が残るのは、フォールバックを設定できない配信先へ置く場合、URL のパス部分を自由に使えない既存アプリへ後から埋め込む場合、同一ページ上の別の仕組みと URL を分け合う場合に限られる。

見落としやすいのは検証のしかたである。History ルーティングのフォールバック漏れは、SPA 内を遷移している限り再現しない。デプロイ後に「深い階層の URL を直接開く」「その URL でリロードする」「ブラウザの戻るで戻る」の 3 つを手で踏むところまでを手順に入れておく。この 3 つが通れば、ルーティングまわりの取り違えはほぼ出尽くす。

この記事は役に立ちましたか?

関連用語

関連する記事