はじめに#
前回の記事では、fetchやaxiosを使ったAPI連携とデータ取得について解説しました。本記事では、複数ページを持つSPA(シングルページアプリケーション)を構築するための「React Router」について詳しく解説します。
従来のWebサイトでは、ページ遷移のたびにブラウザがサーバーへリクエストを送り、新しいHTMLを取得していました。しかし、SPAではJavaScriptがクライアント側でページの表示を切り替えるため、高速でシームレスなユーザー体験を提供できます。React Routerは、このSPAのルーティング機能を実現するための標準的なライブラリです。
本記事を読むことで、以下のことができるようになります。
- React Routerのセットアップと基本設定
- Routes・Routeコンポーネントによるルーティング定義
- 動的ルートとuseParamsによるパラメータ取得
- ネストしたルートとOutletによるレイアウト構成
- Link・NavLinkによる宣言的ナビゲーション
- useNavigateによるプログラマティックな画面遷移
実行環境・前提条件#
必要な環境#
- Node.js 20.x以上
- Viteで作成したReactプロジェクト(TypeScript推奨)
- VS Code(推奨)
前提知識#
- 関数コンポーネントの基本
- useStateとuseEffectの使い方
- Propsによるデータの受け渡し
SPAとは何か - 従来のWebサイトとの違い#
従来のマルチページアプリケーション(MPA)#
従来のWebサイトでは、ページ遷移のたびに以下の処理が行われます。
- ユーザーがリンクをクリック
- ブラウザがサーバーへHTTPリクエストを送信
- サーバーが新しいHTMLを生成して返却
- ブラウザがHTMLを受け取り、ページ全体を再描画
この方式では、ページ遷移のたびに画面が白くなり、ユーザー体験が断片的になります。
シングルページアプリケーション(SPA)#
SPAでは、最初に1つのHTMLファイルを読み込み、その後はJavaScriptがページの表示を切り替えます。
- 初回アクセス時にHTML・CSS・JavaScriptを読み込み
- ユーザーがリンクをクリック
- JavaScriptがURLを更新し、対応するコンポーネントを表示
- 必要に応じてAPIからデータを取得
この方式により、高速でスムーズなページ遷移が実現できます。React Routerは、このURLとコンポーネントの対応関係(ルーティング)を管理するライブラリです。
React Routerのセットアップ#
パッケージのインストール#
まず、React Routerをプロジェクトにインストールします。2024年末にリリースされたReact Router v7では、パッケージ名がreact-routerに統一されました。
1
|
npm install react-router
|
BrowserRouterによるアプリケーションのラップ#
React Routerを使用するには、アプリケーション全体をBrowserRouterコンポーネントでラップします。main.tsx(またはエントリーポイント)を以下のように修正してください。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
|
BrowserRouterは、HTML5のHistory APIを使用してURLを管理します。これにより、ブラウザの戻る・進むボタンが正しく動作し、クリーンなURLが実現できます。
基本的なルーティングの設定#
RoutesとRouteコンポーネント#
ルーティングは、RoutesとRouteコンポーネントを使って定義します。Routesはルート定義のコンテナで、Routeは個々のURLパターンとコンポーネントの対応を表します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import { Routes, Route } from 'react-router';
// ページコンポーネント
function Home() {
return <h1>ホームページ</h1>;
}
function About() {
return <h1>このサイトについて</h1>;
}
function Contact() {
return <h1>お問い合わせ</h1>;
}
// メインのAppコンポーネント
function App() {
return (
<div>
<h1>My App</h1>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</div>
);
}
export default App;
|
上記の設定により、以下のルーティングが有効になります。
| URL |
表示されるコンポーネント |
/ |
<Home /> |
/about |
<About /> |
/contact |
<Contact /> |
indexルート#
index属性を使用すると、親ルートのURLでデフォルトとして表示される子ルートを定義できます。
1
2
3
4
5
6
7
|
<Routes>
<Route path="/" element={<Layout />}>
{/* / にアクセスした時に表示される */}
<Route index element={<Home />} />
<Route path="about" element={<About />} />
</Route>
</Routes>
|
Linkによるナビゲーション#
Linkコンポーネントの基本#
ページ間を移動するには、HTMLの<a>タグではなく、React RouterのLinkコンポーネントを使用します。LinkはSPA内でのナビゲーションを行い、ページ全体の再読み込みを防ぎます。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import { Link } from 'react-router';
function Navigation() {
return (
<nav>
<ul>
<li><Link to="/">ホーム</Link></li>
<li><Link to="/about">About</Link></li>
<li><Link to="/contact">お問い合わせ</Link></li>
</ul>
</nav>
);
}
|
Linkとaタグの違い#
<a>タグと<Link>コンポーネントの違いを理解しておきましょう。
| 特性 |
<a> タグ |
<Link> コンポーネント |
| ページ遷移 |
ページ全体を再読み込み |
クライアントサイドルーティング |
| 速度 |
遅い(サーバーリクエスト) |
高速(JavaScript処理) |
| 状態の保持 |
失われる |
保持される |
| 用途 |
外部サイトへのリンク |
SPA内のページ遷移 |
NavLinkによるアクティブ状態の表示#
現在のページに対応するナビゲーションリンクにスタイルを適用したい場合は、NavLinkコンポーネントを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
import { NavLink } from 'react-router';
function Navigation() {
return (
<nav>
<NavLink
to="/"
className={({ isActive }) => isActive ? 'active' : ''}
>
ホーム
</NavLink>
<NavLink
to="/about"
className={({ isActive }) => isActive ? 'active' : ''}
>
About
</NavLink>
<NavLink
to="/contact"
style={({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
color: isActive ? 'blue' : 'black'
})}
>
お問い合わせ
</NavLink>
</nav>
);
}
|
NavLinkは自動的に現在のURLと比較し、一致する場合はisActiveがtrueになります。classNameやstyleに関数を渡すことで、アクティブ状態に応じたスタイリングが可能です。
また、NavLinkがアクティブな場合は自動的に.activeクラスが付与されるため、CSSで以下のようにスタイルを定義することもできます。
1
2
3
4
|
a.active {
color: blue;
font-weight: bold;
}
|
動的ルートとuseParams#
動的セグメントとは#
実際のアプリケーションでは、/users/1、/users/2のように、URLの一部が動的に変化するケースが多くあります。React Routerでは、パスの一部を:から始めることで「動的セグメント」として定義できます。
1
|
<Route path="/users/:userId" element={<UserDetail />} />
|
この設定により、/users/1、/users/abcなど、/users/以降の任意の文字列にマッチするようになります。
useParamsでパラメータを取得#
動的セグメントの値は、useParamsフックで取得できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import { useParams } from 'react-router';
interface UserParams {
userId: string;
}
function UserDetail() {
// URLパラメータを取得
const { userId } = useParams<UserParams>();
return (
<div>
<h1>ユーザー詳細</h1>
<p>ユーザーID: {userId}</p>
</div>
);
}
|
複数の動的パラメータ#
1つのルートに複数の動的セグメントを含めることも可能です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// ルート定義
<Route path="/categories/:categoryId/products/:productId" element={<ProductDetail />} />
// コンポーネント
function ProductDetail() {
const { categoryId, productId } = useParams();
return (
<div>
<p>カテゴリID: {categoryId}</p>
<p>商品ID: {productId}</p>
</div>
);
}
|
実践例: ブログ記事詳細ページ#
動的ルートを使った実践的な例として、ブログ記事の詳細ページを実装してみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
import { Routes, Route, Link, useParams } from 'react-router';
// 記事データ(実際はAPIから取得)
const posts = [
{ id: '1', title: 'React入門', content: 'Reactの基本を解説します。' },
{ id: '2', title: 'TypeScript入門', content: 'TypeScriptの基本を解説します。' },
{ id: '3', title: 'React Router入門', content: 'ルーティングの基本を解説します。' },
];
// 記事一覧コンポーネント
function PostList() {
return (
<div>
<h1>記事一覧</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
</div>
);
}
// 記事詳細コンポーネント
function PostDetail() {
const { postId } = useParams<{ postId: string }>();
const post = posts.find(p => p.id === postId);
if (!post) {
return <p>記事が見つかりませんでした。</p>;
}
return (
<article>
<h1>{post.title}</h1>
<p>{post.content}</p>
<Link to="/posts">一覧に戻る</Link>
</article>
);
}
// Appコンポーネント
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/posts" element={<PostList />} />
<Route path="/posts/:postId" element={<PostDetail />} />
</Routes>
);
}
|
ネストしたルートとOutlet#
ネストしたルートとは#
複数のページで共通のレイアウト(ヘッダー、サイドバーなど)を使用したい場合、ネストしたルートが便利です。親ルートでレイアウトを定義し、子ルートで各ページのコンテンツを表示します。
1
2
3
4
5
6
7
|
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="settings" element={<Settings />} />
<Route path="profile" element={<Profile />} />
</Route>
</Routes>
|
この設定により、以下のURLが有効になります。
| URL |
表示されるコンポーネント |
/dashboard |
DashboardLayout + DashboardHome |
/dashboard/settings |
DashboardLayout + Settings |
/dashboard/profile |
DashboardLayout + Profile |
Outletコンポーネント#
親ルートのコンポーネントでは、Outletコンポーネントを使って子ルートのコンテンツを表示する場所を指定します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
import { Outlet, Link } from 'react-router';
function DashboardLayout() {
return (
<div className="dashboard">
<header>
<h1>ダッシュボード</h1>
<nav>
<Link to="/dashboard">ホーム</Link>
<Link to="/dashboard/settings">設定</Link>
<Link to="/dashboard/profile">プロフィール</Link>
</nav>
</header>
<main>
{/* 子ルートのコンポーネントがここに表示される */}
<Outlet />
</main>
<footer>
<p>Dashboard Footer</p>
</footer>
</div>
);
}
function DashboardHome() {
return <h2>ダッシュボードへようこそ</h2>;
}
function Settings() {
return <h2>設定ページ</h2>;
}
function Profile() {
return <h2>プロフィールページ</h2>;
}
|
レイアウトルート(pathなしのルート)#
path属性を省略したルートは「レイアウトルート」として機能し、URLに影響を与えずに共通レイアウトを適用できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<Routes>
{/* レイアウトルート(URLには影響しない) */}
<Route element={<MarketingLayout />}>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Route>
{/* 別のレイアウトを適用 */}
<Route element={<AdminLayout />}>
<Route path="/admin" element={<AdminHome />} />
<Route path="/admin/users" element={<AdminUsers />} />
</Route>
</Routes>
|
この設定では、/、/about、/contactはすべてMarketingLayout内に表示され、/admin、/admin/usersはAdminLayout内に表示されます。
useNavigateによるプログラマティックな遷移#
useNavigateとは#
ユーザーのクリックではなく、プログラムの処理結果に応じてページ遷移を行いたい場合があります。例えば、フォーム送信後のリダイレクトや、ログイン成功後のダッシュボードへの遷移などです。このような場合はuseNavigateフックを使用します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
import { useNavigate } from 'react-router';
function LoginForm() {
const navigate = useNavigate();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// ログイン処理(仮)
const success = await performLogin();
if (success) {
// ダッシュボードへ遷移
navigate('/dashboard');
}
};
return (
<form onSubmit={handleSubmit}>
{/* フォームの内容 */}
<button type="submit">ログイン</button>
</form>
);
}
|
navigateのオプション#
navigate関数には、いくつかのオプションを指定できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 基本的な遷移
navigate('/dashboard');
// 履歴を置き換える(戻るボタンで戻れなくする)
navigate('/dashboard', { replace: true });
// 履歴を前後に移動
navigate(-1); // 1つ前のページへ(戻るボタンと同じ)
navigate(1); // 1つ次のページへ(進むボタンと同じ)
navigate(-2); // 2つ前のページへ
// stateを渡す
navigate('/checkout', { state: { from: 'cart' } });
|
実践例: フォーム送信後のリダイレクト#
商品をカートに追加した後、カートページへリダイレクトする例を見てみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
import { useState } from 'react';
import { useNavigate } from 'react-router';
interface Product {
id: string;
name: string;
price: number;
}
function ProductPage({ product }: { product: Product }) {
const navigate = useNavigate();
const [isAdding, setIsAdding] = useState(false);
const handleAddToCart = async () => {
setIsAdding(true);
try {
// カートに追加する処理(仮)
await addToCart(product.id);
// カートページへ遷移
navigate('/cart', {
state: { message: `${product.name}をカートに追加しました` }
});
} catch (error) {
console.error('カートへの追加に失敗しました', error);
} finally {
setIsAdding(false);
}
};
return (
<div>
<h1>{product.name}</h1>
<p>価格: {product.price}円</p>
<button onClick={handleAddToCart} disabled={isAdding}>
{isAdding ? '追加中...' : 'カートに追加'}
</button>
</div>
);
}
|
URLSearchParamsの活用#
useSearchParamsフック#
URLのクエリパラメータ(?key=value)を扱うには、useSearchParamsフックを使用します。検索機能やフィルタリング機能の実装に便利です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import { useSearchParams } from 'react-router';
function SearchResults() {
const [searchParams, setSearchParams] = useSearchParams();
// クエリパラメータを取得
const query = searchParams.get('q') || '';
const category = searchParams.get('category') || 'all';
const handleCategoryChange = (newCategory: string) => {
// クエリパラメータを更新
setSearchParams({ q: query, category: newCategory });
};
return (
<div>
<p>検索キーワード: {query}</p>
<p>カテゴリ: {category}</p>
<select
value={category}
onChange={(e) => handleCategoryChange(e.target.value)}
>
<option value="all">すべて</option>
<option value="electronics">電子機器</option>
<option value="books">書籍</option>
</select>
</div>
);
}
|
404ページの実装#
キャッチオールルート#
存在しないURLにアクセスされた場合に表示する404ページは、*(スプラット)を使って実装します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
function NotFound() {
return (
<div>
<h1>404 - ページが見つかりません</h1>
<p>お探しのページは存在しないか、移動した可能性があります。</p>
<Link to="/">ホームに戻る</Link>
</div>
);
}
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
{/* 他のすべてのURLにマッチ */}
<Route path="*" element={<NotFound />} />
</Routes>
);
}
|
path="*"は、他のどのルートにもマッチしなかったURLをすべてキャッチします。必ずルート定義の最後に配置してください。
完成サンプル: ブログアプリケーション#
ここまで学んだ内容を組み合わせた、完成形のサンプルコードを見てみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
|
import {
Routes,
Route,
Link,
NavLink,
Outlet,
useParams,
useNavigate
} from 'react-router';
// データ(実際はAPIから取得)
const posts = [
{ id: '1', title: 'React入門', content: 'Reactの基本を解説します。', author: 'Alice' },
{ id: '2', title: 'TypeScript入門', content: 'TypeScriptの基本を解説します。', author: 'Bob' },
{ id: '3', title: 'React Router入門', content: 'ルーティングの基本を解説します。', author: 'Alice' },
];
// レイアウトコンポーネント
function Layout() {
return (
<div className="app">
<header>
<h1>My Blog</h1>
<nav>
<NavLink to="/" end className={({ isActive }) => isActive ? 'active' : ''}>
ホーム
</NavLink>
<NavLink to="/posts" className={({ isActive }) => isActive ? 'active' : ''}>
記事一覧
</NavLink>
<NavLink to="/about" className={({ isActive }) => isActive ? 'active' : ''}>
About
</NavLink>
</nav>
</header>
<main>
<Outlet />
</main>
<footer>
<p>2025 My Blog. All rights reserved.</p>
</footer>
</div>
);
}
// ホームページ
function Home() {
return (
<div>
<h2>ようこそ</h2>
<p>このブログではReactに関する情報を発信しています。</p>
<Link to="/posts">記事を読む</Link>
</div>
);
}
// 記事一覧
function PostList() {
return (
<div>
<h2>記事一覧</h2>
<ul>
{posts.map(post => (
<li key={post.id}>
<Link to={`/posts/${post.id}`}>
{post.title} - by {post.author}
</Link>
</li>
))}
</ul>
</div>
);
}
// 記事詳細
function PostDetail() {
const { postId } = useParams<{ postId: string }>();
const navigate = useNavigate();
const post = posts.find(p => p.id === postId);
if (!post) {
return (
<div>
<h2>記事が見つかりません</h2>
<button onClick={() => navigate('/posts')}>一覧に戻る</button>
</div>
);
}
return (
<article>
<h2>{post.title}</h2>
<p className="author">著者: {post.author}</p>
<div className="content">{post.content}</div>
<button onClick={() => navigate(-1)}>戻る</button>
</article>
);
}
// Aboutページ
function About() {
return (
<div>
<h2>このサイトについて</h2>
<p>React学習のためのブログサイトです。</p>
</div>
);
}
// 404ページ
function NotFound() {
return (
<div>
<h2>404 - ページが見つかりません</h2>
<Link to="/">ホームに戻る</Link>
</div>
);
}
// メインApp
function App() {
return (
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<Home />} />
<Route path="/posts" element={<PostList />} />
<Route path="/posts/:postId" element={<PostDetail />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
);
}
export default App;
|
このサンプルでは、以下の機能を実装しています。
- 共通レイアウト(ヘッダー、フッター)
- NavLinkによるアクティブ状態の表示
- 動的ルートによる記事詳細ページ
- useNavigateによる戻るボタンの実装
- 404ページのハンドリング
よくあるエラーと解決策#
BrowserRouterが見つからないエラー#
1
|
Error: useNavigate() may be used only in the context of a <Router> component.
|
このエラーは、useNavigateやLinkなどのReact Routerのフックやコンポーネントが、BrowserRouterの外で使用された場合に発生します。
解決策として、main.tsxでアプリケーション全体をBrowserRouterでラップしてください。
本番環境での404エラー#
SPAをNetlifyやVercelにデプロイした際、直接URLにアクセスすると404エラーになることがあります。これは、サーバーがそのパスのHTMLファイルを探そうとするためです。
解決策として、すべてのリクエストをindex.htmlにリダイレクトする設定が必要です。
Netlifyの場合、public/_redirectsファイルを作成します。
Vercelの場合、vercel.jsonを作成します。
1
2
3
4
5
|
{
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
]
}
|
まとめ#
本記事では、React Routerを使ったSPAのルーティングについて解説しました。
BrowserRouterでアプリケーションをラップし、ルーティングを有効化
RoutesとRouteでURLとコンポーネントの対応を定義
LinkとNavLinkで宣言的なナビゲーションを実装
useParamsで動的ルートのパラメータを取得
Outletでネストしたルートのレイアウトを構成
useNavigateでプログラマティックな画面遷移を実現
React Routerをマスターすることで、複数ページを持つ本格的なWebアプリケーションを構築できるようになります。次の記事では、アプリケーション全体で状態を共有するための「グローバル状態管理」について解説します。
次に読む記事#
参考リンク#