はじめに
「React RouterのcreateBrowserRouterのルーティングってどうなってるんだろう」って皆さん思いますよね。私も思います。
この記事ではブラウザのHistory APIを起点に、createBrowserRouterを中心としたreact-router-domによるルーティングについて首を突っ込んだり突っ込まなかったりします。
1. まず「普通のWebアプリ」の動きを確認する
React Routerの話に入る前に、SPAでない通常のHTMLアプリがどう動くかを整理します。
<a> タグをクリックすると何が起きるか
<a href="/about">Aboutページへ</a>
このリンクをクリックすると、ブラウザは次の手順を踏みます。
① 現在のページをアンロード
② /about へ HTTP リクエストを送る
③ サーバーから HTML を受け取る
④ 新しい HTML でページ全体を描画し直す
ページ全体がリロードされます。 JavaScriptの変数もDOMの状態もすべてリセットされます。
このとき、ブラウザは内部で履歴スタックというリストに新しい履歴エントリを追加しています。
ブラウザの戻るボタンを押すと何が起きるか
戻るボタンを押すと、ブラウザは履歴スタックのひとつ前のエントリを見て、そのURLに対してまたHTTPリクエストを送ります。
2. SPAへの疑問
前節の通り、<a> タグでもブラウザの戻る/進むボタンでもGETリクエストを送信するわけですが、当然SPAでは名前の通りGETリクエストは送信されません。これはどのように制御され、また画面描画を行っているのでしょうか。
本題に入る前に前提として必要なブラウザの History API について調べました。
3. History APIの主要機能
History APIには、SPAのルーティングを支える重要な機能があります。
3-1. pushState:リロードなしでURLを変える
history.pushState() を呼ぶと、ページをリロードせずにURLを変更して、履歴スタックに新しいエントリを追加できます。
// ページのリロードなしに URL が /about に変わる
history.pushState({}, "", "/about");
// state, title, urlの順で引数を指定
ブラウザのアドレスバーにはちゃんと /about が表示されますが、HTTPリクエストは発生しません。
3-2. replaceState:履歴を増やさずURLを変える
history.replaceState() は pushState と似ていますが、履歴スタックに新しいエントリを追加せず、現在のエントリを置き換えます。
// 現在の履歴エントリを /about に置き換える(スタックは増えない)
history.replaceState({}, "", "/about");
pushState との違いは履歴スタックへの影響です。
| メソッド | 履歴スタック |
|---|---|
pushState |
新しいエントリを追加 |
replaceState |
現在のエントリを置き換え |
replaceState 後は戻るボタンを押しても置き換え前のページには戻れません。リダイレクトや検索フォームの絞り込みなど、「この画面は履歴に残したくない」ケースに使います。
3-3. popstate イベント:戻る・進むを検知する
ユーザーがブラウザの戻る・進むボタンを押すと、popstate イベントが発火します。
window.addEventListener("popstate", (event) => {
// 戻る・進むボタンが押されたときに呼ばれる
console.log("現在のURL:", location.pathname);
});
これを使えば、ボタンが押されたタイミングでアプリ側の処理を走らせることができます。
pushState / replaceState では popstate は発火しません。
これらを組み合わせることで、「ページをリロードせずにURLを管理する」ことが可能になります。
4. React Router(createBrowserRouter)はどう使っているか
では、React Routerがこれをどう活用しているか見ていきましょう。
前提:ルーターは location という状態を持っている
createBrowserRouter が生成するルーターは、内部に location という状態を管理しています。これは「現在どのURLにいるか」を表すオブジェクトです。
そして <RouterProvider router={router} /> は、この location が変わるたびに再レンダリングされます。
location が更新される → RouterProvider が再レンダリング → 対応するコンポーネントが表示される
4-1. <Link> や navigate でのページ移動
<Link to="/about"> をクリックしたとき、JS(React Router)が起点となってブラウザに指示を出します。
ポイントは、pushState と location の更新が別々に行われている点です。pushState はあくまでブラウザの履歴を管理するためのものであり、再レンダリングのトリガーは location の更新です。
4-2. 戻る・進むボタンの挙動
<Link> クリックのときは React Router が最初から主導権を持っていました。しかし戻る・進むボタンは、まずブラウザが独自に履歴スタックのポインタを移動させます。React Router はその結果として発火する popstate イベントを受け取り、そこで初めて location を更新します。
今までアドレスバーの変更を検知して再レンダリングが走っていると思っていましたが、それは4-2のみの話でした。4-1では3節にある通り「pushstateでpopstateは発火しない」ので、自力でlocationを更新する結果、再レンダリングされるだけでアドレスバーとUIの更新は独立した処理になっているようです。
補足:state と replace オプション
<Link> や navigate には、3節で紹介した History API の機能に対応したオプションがあります。
-
state:遷移先に任意のデータを渡せます。内部ではpushStateの第一引数として渡され、遷移先でuseLocation().stateから取得できます。 -
replace:trueにすると内部でreplaceStateを使い、履歴スタックを増やさずに遷移します。
navigate("/about", { state: { from: "home" }, replace: true });
まとめ
- ルーターは内部に
locationという状態を持ち、RouterProviderはそれが変わるたびに再レンダリングされる -
<Link>クリック時は ①pushState→ URL更新、②locationを直接更新して再レンダリング -
戻る・進むボタン時は ブラウザが勝手にURL更新 →
popstateイベントを受け取る →locationを更新して再レンダリングを起こす

