React Router v6+ を使用したシングルページアプリケーション(SPA)のページ遷移実装 ガイド
はじめに:SPAとルーティングの必要性
ウェブ開発において、「シングルページアプリケーション」(SPA)は現代の主流なアーキテクチャの一つとなっています。従来の「マルチページアプリケーション」(MPA)が、ページ遷移のたびにサーバーから新しいHTMLドキュメントを取得してブラウザ全体を再読み込みするのに対し、SPAは最初のHTML読み込み以降、JavaScriptを使用して動的にコンテンツを書き換えます。これにより、ユーザーはネイティブアプリケーションのような滑らかな操作感、高速な応答性を体験できます。
しかし、SPAには一つの課題があります。それは、ブラウザのアドレスバーに表示されるURLと、画面に表示されている内容との関連性をどのように維持するか、ということです。MPAではURLの変更がページの再読み込みと直接的に紐づいていましたが、SPAではコンテンツの切り替えはJavaScriptで行われるため、URLは静的なままです。これでは、ユーザーが特定の状態(特定の記事ページ、設定画面など)をブックマークしたり、ブラウザの戻る/進むボタンを使用したり、他のユーザーに現在のページを共有したりすることができません。これはウェブアプリケーションとして重要な機能であり、無視できません。
この課題を解決するのが「クライアントサイドルーティング」です。クライアントサドルーティングは、ブラウザの履歴API(History API)などを利用して、ページの完全な再読み込みなしにURLを更新し、そのURLに基づいて表示するコンポーネントを切り替える仕組みです。これにより、SPAでありながら、URLと画面内容の同期、ブラウザの履歴管理、ブックマークや共有といったMPAが持っていた利便性を再現することができます。
Reactエコシステムにおいて、このクライアントサドルーティングの実装を担うデファクトスタンダードなライブラリが「React Router」です。React Routerは、Reactコンポーネントの宣言的な方法でルーティングを定義・管理するための強力かつ柔軟なツールキットを提供します。本記事では、React Routerの最新バージョンであるv6+に焦点を当て、その基本的な使い方から応用的なテクニックまでを網羅的に解説し、SPAにおけるページ遷移の実装方法を詳細に説明します。
React Router とは
React Router は、Reactアプリケーションにおけるルーティングを管理するためのライブラリです。DOM環境(ブラウザ)で使用する場合は react-router-dom
パッケージを使用します。これは react-router
のコア機能に加えて、ブラウザのDOM環境に特化したコンポーネント(BrowserRouter
, Link
など)を含んでいます。
React Router v6 は、v5以前のバージョンから大きく設計が変更され、よりシンプルで強力、そしてパフォーマンスが向上しています。主な変更点として、Switch
が Routes
に、useHistory
が useNavigate
に、component
プロップが element
プロップに変更され、ネストされたルートの扱いがより直感的になりました。本記事のコード例は全て React Router v6+ に基づいて記述します。
準備:React Router のインストール
React Router を使用するには、プロジェクトに react-router-dom
パッケージをインストールする必要があります。npm または Yarn を使ってインストールできます。
“`bash
npmを使用する場合
npm install react-router-dom
Yarnを使用する場合
yarn add react-router-dom
“`
これで、Reactアプリケーション内で React Router のコンポーネントやフックを使用する準備が整いました。
基本的なルーティングの設定
React Router をアプリケーションで使用するための最初のステップは、ルーティングを管理する「ルーター」(Router)コンポーネントをアプリケーションのルート(最上位)またはルーティングが必要な部分に配置することです。ブラウザ環境では、主に BrowserRouter
が使用されます。
BrowserRouter
は、HTML5 History API を利用して、UI と URL を同期させます。URLのパス部分(例: https://example.com/about
の /about
)を使用してルーティングを行います。ほとんどのウェブアプリケーションでは BrowserRouter
を使用するのが一般的です。
別のルーターとして HashRouter
があります。これはURLのハッシュ部分(例: https://example.com/#/about
の #/about
)を使用してルーティングを行います。これはHistory APIをサポートしない古いブラウザや、サーバーサイドでルーティングの設定が難しい環境(例えば、静的なファイルサーバーで全てのパスを index.html
にフォールバックさせる設定ができない場合など)で役立ちます。しかし、URLに #
が入るため見た目が少し不自然になること、ハッシュ部分の変更はサーバーに送信されない(SEOに不利な場合がある)ことなどから、特別な理由がない限り BrowserRouter
を推奨します。
ここでは BrowserRouter
を使用することを前提に進めます。アプリケーションのエントリーポイント(通常 src/index.js
や src/main.jsx
)で、アプリケーションのルートコンポーネント(多くの場合 App
コンポーネント)を BrowserRouter
でラップします。
“`jsx
// src/index.js または src/main.jsx
import React from ‘react’;
import ReactDOM from ‘react-dom/client’;
import { BrowserRouter } from ‘react-router-dom’;
import App from ‘./App’;
import ‘./index.css’;
const root = ReactDOM.createRoot(document.getElementById(‘root’));
root.render(
{/ アプリケーション全体を BrowserRouter でラップ /}
);
“`
これで、App
コンポーネントとその子コンポーネント内で React Router の機能を使用できるようになります。
ルートの定義とコンポーネントのレンダリング
ルーティングの核となるのは、どのURLパスに対してどのReactコンポーネントを表示するかを定義することです。React Router v6 では、これを <Routes>
と <Route>
コンポーネントを使って行います。
<Routes>
: 複数の<Route>
コンポーネントをグループ化するために使用します。これは以前のバージョンの<Switch>
に相当しますが、よりスマートなルートマッチング(最適なルートを自動的に選択する)を行います。<Route>
: 特定のURLパスと、そのパスがマッチした場合にレンダリングされるコンポーネントを関連付けます。
これらのコンポーネントは、通常、アプリケーションのレイアウトを定義するコンポーネント内(例えば App.js
や Layout.js
といったファイル)に配置します。
“`jsx
// src/App.js
import React from ‘react’;
import { Routes, Route } from ‘react-router-dom’;
import HomePage from ‘./pages/HomePage’; // 後で作成するコンポーネント
import AboutPage from ‘./pages/AboutPage’;
import ContactPage from ‘./pages/ContactPage’;
import NotFoundPage from ‘./pages/NotFoundPage’;
import Navigation from ‘./components/Navigation’; // 後で作成するコンポーネント
import ‘./App.css’;
function App() {
return (
{/* ルート定義のセクション */}
<Routes>
{/* パスが "/" の場合に HomePage をレンダリング */}
<Route path="/" element={<HomePage />} />
{/* パスが "/about" の場合に AboutPage をレンダリング */}
<Route path="/about" element={<AboutPage />} />
{/* パスが "/contact" の場合に ContactPage をレンダリング */}
<Route path="/contact" element={<ContactPage />} />
{/* 上記のどのパスにもマッチしない場合に NotFoundPage をレンダリング */}
<Route path="*" element={<NotFoundPage />} />
</Routes>
</div>
);
}
export default App;
“`
Route
コンポーネントには主に二つの重要なプロップがあります。
path
: マッチさせたいURLのパスを指定します。/
,/about
,/contact
のように記述します。特別なパスとして*
は「任意のパスにマッチ」することを意味し、通常は他のどのルートにもマッチしなかった場合の404ページなどに使用します。element
:path
がマッチした場合にレンダリングしたいReact要素(コンポーネントのインスタンス)を指定します。<HomePage />
のようにJSXで記述します。
<Routes>
は、現在のURLに最もマッチする <Route>
を探し、その element
プロップで指定されたコンポーネントをレンダリングします。Routes
はデフォルトで最も具体的なパスを優先的にマッチさせるため、以前のバージョンの exact
プロップのような厳密なマッチングを指定する必要がほとんどの場合でなくなりました。例えば、/users/123
は /users/:id
にマッチし、/users
にはマッチしません(/users
が存在するルートとして定義されていれば)。しかし、/users
は /users/:id
よりも具体的なパスではないため、/users
にマッチする Route
は /users/:id
より 前に 定義されていなくても正しくマッチします。Routes
が賢く処理してくれます。ただし、*
ルート(404用)は他のどのルートにもマッチしないように、常に <Routes>
の最後に配置するのが良い習慣です。
ナビゲーションの実装:ページ間の移動
React Router を使ってページ間を移動する方法はいくつかあります。最も一般的で推奨されるのは、Link
コンポーネントを使用する方法です。プログラム的に遷移させたい場合は、useNavigate
フックを使用します。
1. Link
コンポーネントを使用したナビゲーション
<Link>
コンポーネントは、HTMLの <a>
タグに似ていますが、SPAのクライアントサイドルーティング向けに最適化されています。クリックしてもページの完全な再読み込みは行わず、History API を使用してURLを変更し、React Router がそれに応じてコンポーネントを切り替えます。
<Link>
コンポーネントは to
プロップを使用します。ここに遷移先のパスを指定します。
“`jsx
// src/components/Navigation.js
import React from ‘react’;
import { Link } from ‘react-router-dom’;
import ‘./Navigation.css’; // スタイルシート
function Navigation() {
return (
);
}
export default Navigation;
“`
この <Navigation>
コンポーネントを App.js
の <Routes>
の上に配置することで、ユーザーはこれらのリンクをクリックしてページ間を移動できるようになります。
なぜ <a>
タグではなく Link
を使うのか?
通常の <a>
タグを使用すると、ブラウザは指定された href
に従ってページの完全な再読み込みを行います。これはSPAの目的である高速な遷移と状態維持に反します。<Link>
は、クリックイベントをインターセプトし、ブラウザのデフォルトの遷移を防ぎ、React Router の内部ルーティングメカニズムをトリガーします。これにより、UIの更新だけが行われ、ページの再読み込みは発生しません。
2. NavLink
コンポーネントを使用したナビゲーション
<NavLink>
は <Link>
の特殊なバージョンです。現在アクティブなルートにマッチする場合に、自動的に特定のCSSクラスやスタイルを適用する機能を持っています。これにより、ナビゲーションバーで現在表示しているページに対応するリンクをハイライト表示するといったことが容易になります。
<NavLink>
は <Link>
の全てのプロップ(to
など)に加えて、いくつかの追加プロップを持ちます。
className
: アクティブでない場合のクラス名を指定します。関数として渡すと、{ isActive: boolean }
オブジェクトを受け取り、それに基づいてクラス名を動的に生成できます。style
: アクティブでない場合のスタイルを指定します。関数として渡すと、{ isActive: boolean }
オブジェクトを受け取り、それに基づいてスタイルを動的に生成できます。
最も一般的な使い方は、アクティブな場合に適用されるクラス名を指定することです。v6では activeClassName
プロップは非推奨になり、className
プロップに { isActive }
オブジェクトを渡す関数として記述する方法が推奨されています。
“`jsx
// src/components/Navigation.js (NavLinkを使用するバージョン)
import React from ‘react’;
import { NavLink } from ‘react-router-dom’;
import ‘./Navigation.css’;
function Navigation() {
return (
);
}
export default Navigation;
“`
そして、対応するCSSを定義します。
“`css
/ src/components/Navigation.css /
.active-link {
font-weight: bold;
color: blue; / 例: アクティブなリンクの色を変える /
}
“`
これで、現在のURLが /
なら「ホーム」リンクがハイライトされ、/about
なら「アバウト」リンクがハイライトされるようになります。
注意点: /
パスに対する NavLink
は、他の全てのパス(/about
, /contact
など)にもマッチしてしまうため、isActive
が常に true
になることがあります。これを避けるためには、v6では end
プロップを使用します。end
を指定すると、そのパスが完全にマッチする場合のみアクティブになります。
“`jsx
end // このプロップを追加
ホーム
“`
これにより、/
の時だけ「ホーム」がアクティブになり、/about
や /contact
の時はアクティブにならなくなります。
3. useNavigate
フックを使用したナビゲーション(プログラムによる遷移)
フォームの送信後、ユーザー登録の成功後、または特定の条件が満たされたときなど、ユーザーのアクションやアプリケーションのロジックに基づいてプログラム的にページ遷移を行いたい場合があります。このような場合、useNavigate
フックを使用します。
useNavigate
フックは、現在のロケーションを変更するための関数を返します。この関数は、遷移先のパスを引数に取ります。
“`jsx
// 例: フォーム送信後にホームページへ遷移するコンポーネント
import React, { useState } from ‘react’;
import { useNavigate } from ‘react-router-dom’;
function MyForm() {
const [value, setValue] = useState(”);
const navigate = useNavigate(); // useNavigate フックを使用
const handleSubmit = (event) => {
event.preventDefault();
// フォームの処理…
console.log(‘送信された値:’, value);
// 処理完了後、ホームページへ遷移
navigate('/');
// 遷移時に履歴を残さず、現在の履歴項目を置き換えたい場合
// navigate('/', { replace: true });
};
return (
);
}
export default MyForm;
“`
navigate
関数はいくつかの形式で呼び出せます。
navigate('/path')
: 指定されたパスに遷移します。これはブラウザの「進む」ボタンで戻れる、新しい履歴項目を追加します。navigate('/path', { replace: true })
: 指定されたパスに遷移しますが、現在の履歴項目を置き換えます。これはユーザーが「戻る」ボタンを押しても、この遷移前のページには戻れなくなります。ログイン後のリダイレクトなど、特定の状況で役立ちます。navigate(-1)
: ブラウザの履歴を1つ戻ります(「戻る」ボタンと同じ)。navigate(1)
: ブラウザの履歴を1つ進みます(「進む」ボタンと同じ)。
useNavigate
フックは関数コンポーネント内でしか使用できません。クラスコンポーネントでプログラム的なナビゲーションが必要な場合は、古い withRouter
HOC (High-Order Component) またはコンテキストを使用する必要がありましたが、関数コンポーネントが主流の現在では useNavigate
が標準的な方法です。
ルートパラメータの利用
SPAでは、特定のアイテム(ユーザー、商品など)の詳細ページを表示するために、URLにIDなどの識別子を含めることがよくあります。例えば、/users/123
の 123
や /products/abc
の abc
のような部分です。これらを「ルートパラメータ」と呼びます。React Router では、ルート定義でルートパラメータを簡単に指定し、コンポーネント内でその値を取得できます。
1. ルートパラメータの定義
<Route>
の path
プロップで、パラメータを定義する部分の前に :
をつけます。
“`jsx
// src/App.js または ルート定義ファイル
import React from ‘react’;
import { Routes, Route } from ‘react-router-dom’;
import HomePage from ‘./pages/HomePage’;
import UserListPage from ‘./pages/UserListPage’;
import UserDetailPage from ‘./pages/UserDetailPage’; // 後で作成
import NotFoundPage from ‘./pages/NotFoundPage’;
import Navigation from ‘./components/Navigation’;
function App() {
return (
{/ ‘/users’ の後に続く任意のセグメントを ‘userId’ というパラメータとして定義 /}
);
}
export default App;
“`
この例では、/users/:userId
というパスを定義しました。:userId
の部分は変数として扱われ、/users/123
や /users/abc
といったパスにマッチします。マッチした際、:userId
にはそれぞれの 123
や abc
といった値が格納されます。
2. ルートパラメータの取得
マッチしたルートコンポーネント内で、URLからルートパラメータの値を取得するには useParams
フックを使用します。
“`jsx
// src/pages/UserDetailPage.js
import React from ‘react’;
import { useParams } from ‘react-router-dom’;
function UserDetailPage() {
// useParams() を呼び出すと、ルートパラメータを格納したオブジェクトが返される
// この場合、{ userId: ‘…’ } の形式になる
const { userId } = useParams();
// ここで userId を使ってAPIからユーザーデータを取得したり、表示を切り替えたりする
// 例: fetchUser(userId);
return (
ユーザー詳細
ユーザーID: {userId}
{/ ここにユーザーデータの表示 /}
);
}
export default UserDetailPage;
“`
useParams
フックは、ルート定義で指定したパラメータ名(例: userId
)をキーとするオブジェクトを返します。上記の例では、パスが /users/123
であれば { userId: '123' }
というオブジェクトが返され、分割代入で userId
変数に 123
が格納されます。
複数のパラメータを定義することも可能です。例えば、/products/:category/:productId
と定義した場合、useParams()
は { category: '...', productId: '...' }
のようなオブジェクトを返します。
ネストされたルート (Nested Routes)
多くのアプリケーションでは、特定のセクション内でさらにサブページやサブビューを持つ複雑なルーティング構造が必要になります。例えば、ダッシュボード内に設定ページ、プロフィールページ、レポートページがある場合などです。React Router v6 は「ネストされたルート」をサポートしており、これを非常に自然に扱うことができます。
ネストされたルートとは、親となる <Route>
の内部に、子となる <Route>
を定義する構造です。これにより、URLセグメントが親ルートと子ルートの定義を組み合わせたものになります。
1. ネストされたルートの定義
親となる <Route>
の内部に、子となる <Route>
を定義します。この際、親ルートの path
は省略できます(相対パスとして扱われます)。子ルートの path
は、親ルートのパスに相対的に追加されます。
“`jsx
// src/App.js または ルート定義ファイル
import React from ‘react’;
import { Routes, Route } from ‘react-router-dom’;
import HomePage from ‘./pages/HomePage’;
import DashboardLayout from ‘./layouts/DashboardLayout’; // ダッシュボード全体のレイアウト
import DashboardHome from ‘./pages/dashboard/DashboardHome’; // /dashboard
import DashboardSettings from ‘./pages/dashboard/DashboardSettings’; // /dashboard/settings
import DashboardProfile from ‘./pages/dashboard/DashboardProfile’; // /dashboard/profile
import NotFoundPage from ‘./pages/NotFoundPage’;
import Navigation from ‘./components/Navigation’;
function App() {
return (
{/* 親ルート: /dashboard */}
{/* path="dashboard/*" のように * をつけると、このルートがそのサブパス全てにマッチすることを示唆しやすくなります */}
<Route path="/dashboard/*" element={<DashboardLayout />}>
{/* 子ルート */}
{/* パスが "dashboard/" の場合、DashboardHome をレンダリング */}
{/* index={true} は、親のパス(/dashboard)に正確にマッチした場合にレンダリングされるデフォルトの子ルートを示します */}
<Route index element={<DashboardHome />} />
{/* パスが "dashboard/settings" の場合、DashboardSettings をレンダリング */}
<Route path="settings" element={<DashboardSettings />} />
{/* パスが "dashboard/profile" の場合、DashboardProfile をレンダリング */}
<Route path="profile" element={<DashboardProfile />} />
{/* ダッシュボード内の404ページ(例: /dashboard/abcdef) */}
<Route path="*" element={<NotFoundPage />} /> {/* または専用の DashboardNotFoundPage */}
</Route>
<Route path="*" element={<NotFoundPage />} /> {/* アプリケーション全体の404 */}
</Routes>
</div>
);
}
export default App;
“`
この例では、/dashboard/*
を親ルートとし、その element
に DashboardLayout
を指定しています。DashboardLayout
がレンダリングされる際、その内部で子ルートがマッチした場合、その子ルートの element
がレンダリングされるようにする必要があります。
2. Outlet
コンポーネントの使用
親ルートの element
コンポーネント(上記の例では DashboardLayout
)内で、マッチした子ルートのコンポーネントをどこにレンダリングするかを示すために、<Outlet>
コンポーネントを使用します。
“`jsx
// src/layouts/DashboardLayout.js
import React from ‘react’;
import { Outlet } from ‘react-router-dom’;
import DashboardNav from ‘../components/DashboardNav’; // ダッシュボード専用ナビゲーション
function DashboardLayout() {
return (
{/ マッチした子ルートのコンポーネントがここにレンダリングされる /}
);
}
export default DashboardLayout;
“`
DashboardLayout
が /dashboard
またはそのサブパスにマッチしてレンダリングされると、<Outlet>
の場所に、現在マッチしている子ルート(/
に対しては DashboardHome
、/settings
に対しては DashboardSettings
など)のコンポーネントがレンダリングされます。
子ルートのパスは、親ルートからの相対パスとして指定できます(例: path="settings"
)。これは、完全なパス(/dashboard/settings
)を指定するよりも簡潔で、親ルートのパスを変更しても子ルートの定義を変更する必要がないため、保守性が向上します。
ネストされたルートは、共通のレイアウト(サイドバー、ヘッダーなど)を持つセクションの実装に非常に有効です。
404(ページが見つかりません)ルートの扱い
ユーザーがアプリケーションに存在しないURLにアクセスした場合に、「ページが見つかりません」(404 Not Found)というエラーページを表示するのはウェブサイトの標準的な振る舞いです。React Router では、これを <Route path="*">
を使用して簡単に実装できます。
path="*"
は、他のどのルートにもマッチしなかった場合にマッチします。したがって、このルートは <Routes>
の定義の中で最後に配置する必要があります。
“`jsx
// src/App.js の Routes 部分(再掲)
{/ ネストされたルートなど、他の全ての具体的なルートの後に配置 /}
“`
そして、NotFoundPage
コンポーネントを作成します。
“`jsx
// src/pages/NotFoundPage.js
import React from ‘react’;
import { useLocation } from ‘react-router-dom’; // 現在のパスを取得する場合
function NotFoundPage() {
const location = useLocation(); // 例: どのパスが見つからなかったか表示する場合
return (
404 – ページが見つかりません
指定されたURL: {location.pathname}
は存在しません。
{/ ホームへのリンクなどを追加することも一般的 /}
{/ Link コンポーネントでもOK /}
);
}
export default NotFoundPage;
“`
このように *
パスを設定することで、アプリケーション全体で存在しないURLへのアクセスを捕捉し、ユーザーに適切なフィードバックを提供できます。
リダイレクトの実装
特定の条件が満たされた場合に、自動的に別のURLにユーザーを遷移させたいことがあります。これを「リダイレクト」と呼びます。React Router v6 では、主に useNavigate
フックまたは <Navigate>
コンポーネントを使用してリダイレクトを実装します。
1. useNavigate
を使用したプログラムによるリダイレクト
既に説明した useNavigate
フックは、イベントハンドラや useEffect
フック内など、JavaScriptのロジックの中でリダイレクトを実行する場合に最適です。
“`jsx
// 例: ログインしていない場合にログインページへリダイレクトする(カスタムフック)
import { useEffect } from ‘react’;
import { useNavigate } from ‘react-router-dom’;
import { useAuth } from ‘./useAuth’; // 例: 認証状態を管理するカスタムフック
function useRequireAuth() {
const auth = useAuth(); // 認証状態を取得 (例: { user, loading })
const navigate = useNavigate();
useEffect(() => {
// 認証チェックが完了し、ユーザーが存在しない場合
if (!auth.loading && !auth.user) {
// ログインページへリダイレクト。replace: true で履歴を残さない
navigate(‘/login’, { replace: true });
}
}, [auth.user, auth.loading, navigate]); // 依存配列に含める
// 必要に応じて認証状態などを返す
return auth;
}
// このフックを、認証が必要なページのコンポーネント内で呼び出す
function ProtectedPage() {
const auth = useRequireAuth();
if (auth.loading) {
return
Loading…
; // 認証チェック中はローディング表示
}
// 認証済みであればコンテンツを表示
if (auth.user) {
return (
保護されたページ
ようこそ、{auth.user.name}さん!
);
}
// 認証されていない場合はリダイレクトされているはずなので、
// ここに到達することは基本的にはないが、フォールバックとしてnullなどを返す
return null;
}
“`
この方法では、コンポーネントのレンダリング中に条件をチェックし、副作用としてリダイレクトを実行できます。
2. <Navigate>
コンポーネントを使用したリダイレクト
<Navigate>
コンポーネントは、レンダーツリー内でリダイレクトを宣言的に行いたい場合に使用します。これは、特定の条件に基づいて別のコンポーネントではなくリダイレクトを実行したい場合に便利です。以前のバージョンの <Redirect>
に相当します。
“`jsx
// 例: ユーザーが既にログインしている場合にダッシュボードへリダイレクトする
import React from ‘react’;
import { Navigate, useLocation } from ‘react-router-dom’;
import { useAuth } from ‘./useAuth’; // 例: 認証状態を管理するカスタムフック
function LoginPage() {
const auth = useAuth(); // 認証状態を取得
const location = useLocation(); // 遷移元情報を取得する場合(ログイン後に元のページに戻すためなど)
// 既にログインしている場合、ダッシュボードにリダイレクト
// replace prop を使用すると、現在の履歴項目を置き換える
if (auth.user) {
// ログイン前のページがあればそこへ、なければダッシュボードへ
const from = location.state?.from?.pathname || “/dashboard”;
return
}
// ログインしていない場合はログインフォームを表示
return (
ログイン
{/ ログインフォームのコード /}
ログインフォームがここに表示されます。
);
}
export default LoginPage;
“`
<Navigate>
コンポーネントがレンダーされると、その to
プロップで指定されたパスに自動的に遷移します。replace
プロップは useNavigate
と同様に履歴を置き換えるかどうかを指定します。<Navigate>
は、コンポーネントのレンダリングロジックの一部として条件付きでリダイレクトを行う場合に便利です。
Search Parameters (クエリ文字列) の利用
URLのパス部分(例: /products
)に加えて、クエリ文字列(Search Parameters)を使ってデータを渡すことがよくあります。これは、フィルタリング、ソート、ページネーションの状態などを保持するために使用されます。例: /products?category=electronics&sort=price_asc
React Router v6 では、useSearchParams
フックを使ってクエリ文字列を簡単に読み書きできます。
useSearchParams
フックは、URLのクエリ文字列を操作するための便利なAPIを提供します。これは useState
フックのように、現在の検索パラメータの状態と、それを更新するための関数をペアで返します。
“`jsx
// 例: クエリ文字列でフィルタリングとソートを行う商品リストページ
import React from ‘react’;
import { useSearchParams } from ‘react-router-dom’;
function ProductListPage() {
// useSearchParams を呼び出すと、[SearchParamsオブジェクト, setSearchParams関数] のペアが返される
const [searchParams, setSearchParams] = useSearchParams();
// クエリパラメータの値を取得
const category = searchParams.get(‘category’); // 例: ‘electronics’
const sort = searchParams.get(‘sort’); // 例: ‘price_asc’
// ここで category と sort の値を使って商品リストをフィルタリング・ソートする
// 例: fetchProducts({ category, sort });
// クエリパラメータを更新する関数
const handleCategoryChange = (newCategory) => {
// 現在のパラメータを保持しつつ、categoryだけ更新
setSearchParams(prevParams => {
prevParams.set(‘category’, newCategory);
return prevParams;
});
};
const handleSortChange = (newSort) => {
// 現在のパラメータを保持しつつ、sortだけ更新
setSearchParams(prevParams => {
prevParams.set(‘sort’, newSort);
return prevParams;
});
};
// 特定のパラメータを削除する場合
const handleRemoveCategory = () => {
setSearchParams(prevParams => {
prevParams.delete(‘category’);
return prevParams;
});
};
return (
商品リスト
現在のフィルター: {category}
現在のソート: {sort}
{/* UI要素からクエリパラメータを更新 */}
<button onClick={() => handleCategoryChange('electronics')}>
カテゴリ:電子機器
</button>
<button onClick={() => handleSortChange('price_desc')}>
ソート:価格(降順)
</button>
<button onClick={handleRemoveCategory}>
カテゴリフィルターを解除
</button>
{/* 商品リストの表示 */}
</div>
);
}
export default ProductListPage;
“`
useSearchParams
は非常に強力で、URLの状態をアプリケーションの状態と同期させるのに役立ちます。searchParams
オブジェクトは URLSearchParams
のインスタンスであり、get()
, getAll()
, has()
, delete()
, set()
といったメソッドを持っています。setSearchParams
関数に新しい URLSearchParams
インスタンスや、既存のインスタンスを操作する関数を渡すことで、URLのクエリ文字列を更新できます。URLの変更は履歴に新しいエントリを追加します。
履歴(History)API と React Router の内部
React Router は、ブラウザの History API(history.pushState
, history.replaceState
, popstate
イベント)や Hashchange イベントを抽象化して提供しています。
BrowserRouter
は History API を使用します。history.pushState(state, title, url)
は新しい状態とURLを履歴スタックに追加し、history.replaceState(state, title, url)
は現在の履歴エントリを置き換えます。popstate
イベントは、ブラウザの戻る/進むボタンが押されたり、JavaScriptで history.go()
, history.back()
, history.forward()
が呼び出されたりした場合に発生します。
React Router はこれらのイベントを監視し、URLの変更を検出します。URLが変更されると、React Router は内部的にどの <Route>
が現在のURLにマッチするかを計算し直し、それに応じて表示すべきコンポーネントを更新(再レンダリング)します。これがSPAのページ遷移の仕組みです。ブラウザはサーバーに新しいHTMLを要求することなく、JavaScriptの実行によってUIが切り替わります。
useNavigate
フックは内部的に History オブジェクトの push
や replace
メソッドを呼び出し、<Link>
コンポーネントもクリック時にこれらのメソッドをトリガーします。
コード分割 (Code Splitting) と Lazy Loading
SPAのパフォーマンス最適化の一つにコード分割があります。これは、アプリケーションの全てのコードを一つの大きなJavaScriptファイルにバンドルするのではなく、ルートごとに小さなチャンクに分割し、必要なときにだけダウンロードするようにする技術です。これにより、初期ロード時間を短縮し、アプリケーションの応答性を向上させることができます。
React Router は、React の React.lazy
と Suspense
機能と組み合わせて、ルートベースのコード分割を簡単に実現できます。
React.lazy()
: 動的にインポートされるコンポーネントを定義します。これにより、コンポーネントが実際にレンダリングされるまでそのコードの読み込みを遅延させることができます。Suspense
: 動的にインポートされたコンポーネントが読み込まれる間に、フォールバック(ローディングインジケーターなど)を表示するために使用します。
これらの機能は、動的 import()
構文をサポートするバンドラー(Webpack, Parcel, Rollupなど)が必要です。Create React App や Vite など、現代的な開発環境はこれを標準でサポートしています。
“`jsx
// src/App.js または ルート定義ファイル
import React, { Suspense, lazy } from ‘react’;
import { Routes, Route } from ‘react-router-dom’;
import HomePage from ‘./pages/HomePage’;
import NotFoundPage from ‘./pages/NotFoundPage’;
import Navigation from ‘./components/Navigation’;
// 動的にインポートされるコンポーネントを定義
// これらのコンポーネントのコードは、初めて必要とされるまでロードされない
const AboutPage = lazy(() => import(‘./pages/AboutPage’));
const ContactPage = lazy(() => import(‘./pages/ContactPage’));
const DashboardLayout = lazy(() => import(‘./layouts/DashboardLayout’));
const DashboardHome = lazy(() => import(‘./pages/dashboard/DashboardHome’));
const DashboardSettings = lazy(() => import(‘./pages/dashboard/DashboardSettings’));
const DashboardProfile = lazy(() => import(‘./pages/dashboard/DashboardProfile’));
function App() {
return (
{/* Suspense でラップして、lazy コンポーネントのロード中に表示するフォールバックを指定 */}
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<HomePage />} />
{/* lazy コンポーネントを element に指定 */}
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<ContactPage />} />
{/* ネストされたlazyルートも可能 */}
<Route path="/dashboard/*" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="settings" element={<DashboardSettings />} />
<Route path="profile" element={<DashboardProfile />} />
{/* ダッシュボード内の404も laz yにするならここも lazyRoute */}
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Suspense>
</div>
);
}
export default App;
“`
<Suspense>
コンポーネントは、その子コンポーネントツリー内で遅延ロードされるものが解決されるまで、fallback
プロップに指定された要素(例えば、ローディングスピナー)を表示します。<Routes>
や個々の <Route element={...}>
を <Suspense>
でラップすることで、特定のルートへの遷移時にそのルートに必要なコンポーネントが読み込まれるまでローディング表示を行うことができます。アプリケーション全体を一つの <Suspense>
でラップすることも、セクションごとに複数の <Suspense>
を配置することも可能です。
エラー境界 (Error Boundaries) とルーティング
SPAでは、特定のコンポーネント内でエラーが発生した場合、アプリケーション全体がクラッシュしてしまう可能性があります。これを防ぐために、Reactの「エラー境界」機能を使用できます。エラー境界は、子コンポーネントツリーで発生したJavaScriptエラーをキャッチし、エラーをログに記録し、フォールバックUIを表示するReactコンポーネントです。
特にルーティングにおいて、特定のルートのコンポーネントのロードやレンダリング中にエラーが発生した場合、そのルートのセクションだけをエラー表示にすることで、アプリケーションの他の部分に影響を与えないようにすることができます。
エラー境界はクラスコンポーネントとして実装する必要があります(React 16以降)。componentDidCatch
または static getDerivedStateFromError
メソッドを実装したコンポーネントがエラー境界となります。
“`jsx
// src/components/ErrorBoundary.js
import React from ‘react’;
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
// エラー発生時に state を更新してフォールバックUIを表示
static getDerivedStateFromError(error) {
return { hasError: true, error: error };
}
// エラーログを記録
componentDidCatch(error, errorInfo) {
console.error(“Caught an error in component:”, error, errorInfo);
this.setState({ errorInfo: errorInfo });
}
render() {
if (this.state.hasError) {
// フォールバックUI
return (
エラーが発生しました。
このページを表示できません。
{/ デバッグ情報(開発時のみ表示など) /}
{process.env.NODE_ENV === ‘development’ && (
{this.state.errorInfo.componentStack}
)}
{/ ホームに戻るリンクなど /}
);
}
// エラーがなければ子コンポーネントを通常通りレンダリング
return this.props.children;
}
}
export default ErrorBoundary;
“`
このエラー境界コンポーネントを、個々の <Route element={...}>
の周囲に配置することで、そのルートコンポーネント内で発生したエラーを捕捉できます。
“`jsx
// src/App.js の Routes 部分 (ErrorBoundary を使用)
import React, { Suspense, lazy } from ‘react’;
import { Routes, Route } from ‘react-router-dom’;
import HomePage from ‘./pages/HomePage’;
import NotFoundPage from ‘./pages/NotFoundPage’;
import Navigation from ‘./components/Navigation’;
import ErrorBoundary from ‘./components/ErrorBoundary’; // ErrorBoundary をインポート
const AboutPage = lazy(() => import(‘./pages/AboutPage’));
const ContactPage = lazy(() => import(‘./pages/ContactPage’));
// … 他の lazy コンポーネント
function App() {
return (
}>
{/* 各ルートを ErrorBoundary でラップ */}
<Route path="/about" element={<ErrorBoundary><AboutPage /></ErrorBoundary>} />
<Route path="/contact" element={<ErrorBoundary><ContactPage /></ErrorBoundary>} />
{/* ネストされたルートの場合、親ルートや子ルートそれぞれに ErrorBoundary を設定できる */}
{/* 例: 親ルートに設定すると、ダッシュボードセクション全体のエラーを捕捉 */}
<Route path="/dashboard/*" element={<ErrorBoundary><DashboardLayout /></ErrorBoundary>}>
{/* 子ルートを個別にラップすることも可能 */}
<Route index element={<DashboardHome />} />
<Route path="settings" element={<DashboardSettings />} />
<Route path="profile" element={<DashboardProfile />} />
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Suspense>
</div>
);
}
export default App;
“`
このようにエラー境界を導入することで、より堅牢なSPAを構築できます。特定のルートやセクションでのエラーが、アプリケーション全体を停止させることを防ぎます。
実践例:シンプルなブログアプリケーション
これまでに説明した React Router の主要な機能を使用して、シンプルなブログアプリケーションのルーティングを実装してみましょう。
アプリケーション構成:
/
: トップページ (記事一覧)/about
: アバウトページ/posts/:postId
: 個別記事ページ- 任意のパス: 404ページ
必要なコンポーネント (仮):
App.js
: ルート定義とナビゲーション、ルーターの配置Navigation.js
: ナビゲーションリンク (NavLink
)HomePage.js
: 記事一覧を表示 (Link
で個別記事へ遷移)AboutPage.js
: シンプルなAboutページPostPage.js
: 個別記事を表示 (useParams
で記事IDを取得)NotFoundPage.js
: 404ページ
コード:
まず、基本的なコンポーネントファイルを作成します(内容はプレースホルダー)。
“`jsx
// src/pages/HomePage.js
import React from ‘react’;
import { Link } from ‘react-router-dom’; // リンク用
// 仮の記事データ
const posts = [
{ id: ‘1’, title: ‘React Router 入門’, content: ‘…’ },
{ id: ‘2’, title: ‘SPA開発の利点’, content: ‘…’ },
];
function HomePage() {
return (
最新記事
-
{posts.map(post => (
-
{/ Link を使って個別記事ページへ遷移 /}
/posts/${post.id}}>{post.title}
))}
);
}
export default HomePage;
// src/pages/AboutPage.js
import React from ‘react’;
function AboutPage() {
return (
このブログについて
これはReact Router v6+ のデモです。
);
}
export default AboutPage;
// src/pages/PostPage.js
import React from ‘react’;
import { useParams } from ‘react-router-dom’; // ルートパラメータ取得用
// 仮の記事データ (HomePageと同じ)
const posts = [
{ id: ‘1’, title: ‘React Router 入門’, content: ‘React Router v6の使い方…’ },
{ id: ‘2’, title: ‘SPA開発の利点’, content: ‘なぜSPAが良いのか…’ },
];
function PostPage() {
// URLから記事IDを取得
const { postId } = useParams();
// 記事IDに基づいて記事データを検索
const post = posts.find(p => p.id === postId);
if (!post) {
// IDが見つからない場合、404ページを表示させることもできる
// または、ここで専用のエラーメッセージを表示
return
;
}
return (
{post.title}
{post.content}
{/ コメントフォームなど /}
);
}
export default PostPage;
// src/pages/NotFoundPage.js (前述と同じ)
import React from ‘react’;
import { useLocation } from ‘react-router-dom’;
function NotFoundPage() {
const location = useLocation();
return (
404 – ページが見つかりません
指定されたURL: {location.pathname}
は存在しません。
ホームに戻る
);
}
export default NotFoundPage;
// src/components/Navigation.js (NavLinkを使用)
import React from ‘react’;
import { NavLink } from ‘react-router-dom’;
import ‘./Navigation.css’; // スタイルは別途用意
function Navigation() {
return (
);
}
export default Navigation;
// src/Navigation.css (例)
/
.active-link {
font-weight: bold;
color: blue;
}
/
“`
次に、これらのコンポーネントを App.js
と index.js
で組み合わせてルーティングを設定します。
“`jsx
// src/App.js
import React from ‘react’;
import { Routes, Route } from ‘react-router-dom’;
import HomePage from ‘./pages/HomePage’;
import AboutPage from ‘./pages/AboutPage’;
import PostPage from ‘./pages/PostPage’;
import NotFoundPage from ‘./pages/NotFoundPage’;
import Navigation from ‘./components/Navigation’;
import ‘./App.css’; // 全体スタイル
function App() {
return (
{/* メインコンテンツエリアにRoutesを配置 */}
<main>
<Routes>
{/* 各ルート定義 */}
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
{/* ルートパラメータ :postId を定義 */}
<Route path="/posts/:postId" element={<PostPage />} />
{/* 上記どのルートにもマッチしない場合の404 */}
<Route path="*" element={<NotFoundPage />} />
</Routes>
</main>
</div>
);
}
export default App;
// src/index.js または src/main.jsx (前述と同じ)
import React from ‘react’;
import ReactDOM from ‘react-dom/client’;
import { BrowserRouter } from ‘react-router-dom’;
import App from ‘./App’;
import ‘./index.css’;
const root = ReactDOM.createRoot(document.getElementById(‘root’));
root.render(
);
“`
この設定により、以下の動作が実現されます。
/
にアクセスするとHomePage
が表示されます。HomePage
の記事タイトルをクリックすると、Link
コンポーネントによってURLが/posts/1
や/posts/2
のようになり、ページ再読み込みなしにPostPage
が表示されます。PostPage
ではuseParams
フックでURLから記事IDを取得し、そのIDに対応する記事内容を表示します。/about
にアクセスするとAboutPage
が表示されます。/abcdef
のような存在しないパスにアクセスするとNotFoundPage
が表示されます。- ナビゲーションバーのリンクは
NavLink
により、現在のページに合わせてハイライトされます。
このシンプルな例は、React Router の基本的な機能である <Route>
, <Routes>
, <Link>
, <NavLink>
, useParams
, BrowserRouter
の使い方を示しています。
React Router 使用上のベストプラクティス
React Router を大規模なアプリケーションで効果的に使用するためのいくつかのベストプラクティスを紹介します。
- ルート定義の一元化: アプリケーションの全てのルート定義を一つのファイルやモジュールに集約すると、アプリケーション全体のルーティング構造を把握しやすくなり、管理が容易になります。特に
<Routes>
を含むコンポーネントを独立させることが推奨されます。 -
パスの定数化: URLパスをハードコーディングする代わりに、定数として定義して使用すると、タイプミスを防ぎ、パスの変更が必要になった場合に一箇所だけ修正すれば済むようになります。
“`javascript
// src/constants/paths.js
export const HOME = ‘/’;
export const ABOUT = ‘/about’;
export const POST_DETAIL = ‘/posts/:postId’;
export const NOT_FOUND = ‘*’;// App.js
import { HOME, ABOUT, POST_DETAIL, NOT_FOUND } from ‘./constants/paths’;
// …
} />
} />
} />
} />
// HomePage.js
import { POST_DETAIL } from ‘./constants/paths’;
import { generatePath } from ‘react-router-dom’; // パラメータ付きパスを生成// …
{post.title}
``
generatePathヘルパー関数は、パラメータ付きのパス定数から実際のURLを生成するのに役立ちます。
React.lazy
3. **ネストされたルートの活用:** 共通のレイアウトを持つセクションでは、積極的にネストされたルートを使用してコードの重複を避け、関連するルート定義をまとめるようにします。
4. **Lazy Loading の適用:** アプリケーションのバンドルサイズが大きくなってきたら、ルートコンポーネントにと
Suspenseを適用して、初期ロード時間を改善することを検討します。
BrowserRouter
5. **エラー境界の設定:** 主要なルートやアプリケーション全体にエラー境界を設定し、予期しないエラー発生時のユーザー体験を向上させます。
6. **サーバーサイド設定:**を使用する場合、ユーザーが特定のサブパス(例:
/about)に直接アクセスした際に、サーバーがそのパスに対応するHTMLファイルを返すのではなく、SPAのエントリーポイント(通常
index.html)を返すようにサーバーを設定する必要があります。これは、History API で変更されたURLはサーバーに送信されないため、ブラウザがサーバーにリクエストするのは常に最初のエントリーポイントURL(例:
/`)であるという前提に基づいています。多くの開発サーバー(Webpack Dev Server, Vite)やホスティングサービス(Netlify, Vercel, Surgeなど)は、このための設定(フォールバック設定)をサポートしています。
よくある問題とトラブルシューティング
React Router の実装中によく遭遇する問題と、その解決策のヒントです。
- ページ遷移が起きない、または完全な再読み込みになる:
BrowserRouter
(またはHashRouter
) がアプリケーションのルートまたは必要な部分を正しくラップしているか確認してください。Link
コンポーネントの代わりに通常の<a>
タグを使用していないか確認してください。<a>
タグを使用する場合は、e.preventDefault()
でデフォルトの遷移をキャンセルし、useNavigate
を使用する必要があります。
useNavigate
,useParams
などのフックがエラーになる:- これらのフックは、
BrowserRouter
などのルーターコンポーネントでラップされたコンポーネントツリーの内部でしか使用できません。フックを使用しているコンポーネントがルーターのスコープ内にあるか確認してください。
- これらのフックは、
- 404ページが常に表示される、または特定のルートがマッチしない:
<Routes>
内の<Route>
の順序を確認してください。より具体的なパスを先に定義する必要はありませんが(v6のRoutes
が最適マッチを選択するため)、*
ルートは常に最後に配置する必要があります。- パス文字列にタイプミスがないか確認してください。ルートパラメータ(
:param
)のスペルも重要です。 - ネストされたルートを使用している場合、親ルートの
path
に/*
を付けると意図しないマッチを引き起こす場合があります。親ルートのpath
は子ルートのパス計算のベースになるため、通常は*
なしで定義し、子ルートのpath
を相対的に定義します(例: 親path="dashboard"
、子path="settings"
->/dashboard/settings
)。ただし、親コンポーネント内で<Routes>
を別途定義する場合はpath="*"
が親コンポーネントのどのサブパスにもマッチすることを示唆するため、有用です。この記事の例では、<Route path="/dashboard/*" element={<DashboardLayout />}>
のように<Routes>
の中で親ルートを定義するパターンを紹介していますが、この場合DashboardLayout
は/dashboard
以下 の全てのパスでレンダリングされ、DashboardLayout
内の<Outlet>
がマッチした子ルートをレンダリングします。この*
は必須ではありませんが、意図を明確にするために使用されることがあります。混乱を避けるため、シンプルなパス定義から始めるのが良いでしょう。 - サーバーサイドの設定で、存在しないパスへのリクエストが
index.html
にフォールバックされているか確認してください。特に本番環境へのデプロイ時に重要です。
NavLink
のアクティブクラスが正しく適用されない:to="/"
のようなルートパスに対しては、end
プロップを追加して正確なマッチングを強制してください。className
プロップに関数を渡し、isActive
の値を確認してデバッグしてください。
- ルートパラメータやクエリ文字列が取得できない:
useParams
やuseSearchParams
が正しいコンポーネント(マッチした<Route element={...}>
の中でレンダリングされるコンポーネント)内で使用されているか確認してください。- ルート定義のパスが、パラメータ名(例:
/users/:userId
)と一致しているか確認してください。 - URLのクエリ文字列が
?key=value&key2=value2
の形式で正しくエンコードされているか確認してください。
まとめ
React Router v6+ は、Reactシングルページアプリケーションにおいて、宣言的かつ柔軟な方法でクライアントサドルーティングを実装するための強力なライブラリです。本記事では、以下の主要な概念と実装方法を詳細に解説しました。
BrowserRouter
を使用した基本的なルーターの設定<Routes>
と<Route>
を使用したルートの定義とコンポーネントのマッピング (path
,element
)<Link>
と<NavLink>
を使用した宣言的なナビゲーションuseNavigate
フックを使用したプログラムによるナビゲーション- ルートパラメータ(
:param
)の定義とuseParams
フックでの取得 <Outlet>
を使用したネストされたルートの実装<Route path="*">
を使用した404ページの処理useSearchParams
フックを使用したクエリ文字列の読み書きReact.lazy
とSuspense
を使用したコード分割と遅延ロード- エラー境界によるエラーハンドリング
React Router を習得することは、現代的なReact SPA開発において非常に重要です。これらの基本的なビルディングブロックを理解し、組み合わせることで、複雑なナビゲーションを持つ大規模なアプリケーションでも、クリーンで保守可能なルーティングコードを記述できるようになります。
実践を通してこれらの概念を使いこなし、あなたのReactアプリケーションにリッチでユーザーフレンドリーなページ遷移を実装してください。React Router の公式ドキュメントも非常に充実していますので、さらに深く学びたい場合は参照することをお勧めします。
これで、React Router v6+ を使用したSPAのページ遷移実装に関する詳細な説明を網羅した記事は完了です。