ルーティング概要(React Router)
SPA のルーティング概念、React Router v7 の基本的な使い方、そして4部の Next.js App Router との動作方式比較を一度に扱います。
14章でパフォーマンス最適化の道具を扱いました。本章は2部の最後の章です。ここまで私たちは 1つの画面 の中で起きることを扱ってきましたが、実際のアプリは普通、複数の画面を持ちます。メニュークリックで画面が変わり、URL が変わり、戻るボタンも動作しなければなりません。こうした画面遷移を扱う道具が ルーティング です。
本章で扱う React Router のモデルは、4部(モダン Next.js)の App Router と比較可能な形です。本章の最後に2つのモデルの意思決定表を置いて、「どの場合に何が適切か」の感覚をつかんでおきます。
伝統的な Web vs SPA #
伝統的な Web ページ は、ユーザーがリンクをクリックするたびにブラウザがサーバーに新しいページをリクエストし、サーバーが作った新しい HTML を受け取って画面全体を再度描いていました。ページ遷移のたびに白い瞬きが起きるあの方式です。
SPA(Single Page Application) は、最初に1度 HTML を受け取ったあと、以降の画面遷移は JavaScript で画面を描き直す方式 です。サーバーに新しい HTML をリクエストせずクライアントが自分で画面を入れ替えるので、遷移が速く滑らかです。
Vite で作った基本の React アプリは SPA です。SPA は最初に受け取ったあの HTML の中ですべての出来事が起きるので、「URL が変わったらどの画面を見せるか」を私たちが直接決める必要があります。これを クライアントサイドルーティング と呼び、React エコシステムでの事実上の標準ライブラリが React Router です。
React Router のインストール #
ここまで使ってきた Vite プロジェクトに React Router を追加します。
pnpm add react-router本書は React Router v7 をベースにします。v7 から単一の react-router パッケージに統合され、古い資料でよく見る react-router-dom はもう別パッケージではありません。古いコードを移行するときは import だけ react-router-dom → react-router に変えれば API はほぼそのまま動きます。
もっともシンプルな例 #
ルーティングの基本構造をまず見てみます。
import { BrowserRouter, Routes, Route, Link } from 'react-router';
function Home() {
return <h1>ホームページ</h1>;
}
function About() {
return <h1>概要ページ</h1>;
}
function App() {
return (
<BrowserRouter>
<nav style={{ padding: '8px', borderBottom: '1px solid #ccc' }}>
<Link to="/">ホーム</Link>
{' | '}
<Link to="/about">概要</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
);
}
export default App;主な要素:
<BrowserRouter>— ルーティングを有効化する最上位のラッパー。アプリ全体を包む<Routes>— 複数の<Route>の中から現在の URL と一致するひとつを選んでレンダリングするコンテナ<Route path="..." element={<...>} />— どのパスにどのコンポーネントを見せるかを定義<Link to="...">— 画面の瞬きなしにルートを切り替えるリンク
<a href="/about"> のような普通の anchor タグを使うと、ブラウザがページを新しく読み込んで SPA の利点を失います。必ず <Link> を使ってこそ クライアントサイドの遷移が起こります。
URL パラメータ — 動的なパス #
商品詳細ページやユーザープロフィールのように、URL の一部が動的に変わるパスがあります。コロン(:)を付けて動的な部分を示します。
<Route path="/users/:userId" element={<UserProfile />} />/users/123、/users/cheolsu のような URL はすべてこのルートにマッチします。コンポーネントの中では useParams フックで動的部分の値を取り出します。
import { useParams } from 'react-router';
function UserProfile() {
const { userId } = useParams();
return <h1>ユーザー ID: {userId}</h1>;
}
export default UserProfile;useParams は path に明示したパラメータをすべてオブジェクトで返します。path="/users/:userId/posts/:postId" なら { userId, postId } を取り出せます。
プログラマティックなナビゲーション — useNavigate #
リンク以外に、コードから直接移動させたいときがあります。フォーム送信後に結果ページへ移動したり、ログアウトボタンがホームへ送ったりするケースです。useNavigate フックを使います。
import { useState } from 'react';
import { useNavigate } from 'react-router';
function LoginForm() {
const [email, setEmail] = useState('');
const navigate = useNavigate();
function handleSubmit(e) {
e.preventDefault();
// ... ログイン処理 ...
navigate('/dashboard');
}
return (
<form onSubmit={handleSubmit}>
<input value={email} onChange={(e) => setEmail(e.target.value)} />
<button type="submit">ログイン</button>
</form>
);
}navigate('/path') で移動、navigate(-1) なら戻る、navigate(1) なら進むです。
クエリパラメータ — useSearchParams #
URL の ?key=value&key2=value2 部分(クエリ文字列)を扱うときは useSearchParams を使います。
import { useSearchParams } from 'react-router';
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('q') ?? '';
return (
<div>
<input
value={query}
onChange={(e) => setSearchParams({ q: e.target.value })}
placeholder="検索"
/>
<p>現在の検索語: {query}</p>
</div>
);
}useSearchParams は useState に似たインターフェイスで動作します。入力に応じて URL 自体が /search?q=react のように更新され、リロードや URL の共有でも同じ状態が復元されます。検索結果ページのように「URL に状態が反映されるべき」ケースに便利です。
ネストされたルートと Outlet #
複数のページが同じレイアウト(ヘッダー、サイドバーなど)を共有するときは、ネストされたルート がきれいです。親のルートが共通レイアウトを描き、子のルートがその中のコンテンツ領域を埋める構造です。
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="users/:userId" element={<UserProfile />} />
</Route>
</Routes>import { Link, Outlet } from 'react-router';
function Layout() {
return (
<div>
<header style={{ padding: '8px', background: '#f4f4f4' }}>
<Link to="/">ホーム</Link>
{' | '}
<Link to="/about">概要</Link>
</header>
<main style={{ padding: '16px' }}>
<Outlet />
</main>
</div>
);
}<Outlet /> の位置に子ルートのコンポーネントがレンダリングされます。<Route index> は親のパス(/)と正確にマッチしたときに見せる子を意味します。
このパターンのおかげで、ヘッダー / フッターのコードを1か所に置きつつ、URL に応じて真ん中のコンテンツだけ変えられます。4部の Next.js App Router も同じ動作原理(layout + 子ページ)をファイルシステムベースで自動化したものです。
404 ページ #
マッチするルートがないときのページを作るには、path="*" のワイルドカードルートを最後に置きます。
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Routes>上から順にマッチを試み、どれにも合わなければ * が拾います。
アクティブなリンクの表示 — NavLink #
ナビゲーションバーで現在のページのリンクを強調したいときは、<Link> の代わりに <NavLink> を使います。
import { NavLink } from 'react-router';
function Nav() {
return (
<nav>
<NavLink
to="/"
end
style={({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
color: isActive ? 'tomato' : 'inherit',
})}
>
ホーム
</NavLink>
{' | '}
<NavLink
to="/about"
style={({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
color: isActive ? 'tomato' : 'inherit',
})}
>
概要
</NavLink>
</nav>
);
}style(または className)に関数を渡すと、isActive 情報を受け取って分岐できます。end prop は「ちょうどこのパスのときだけアクティブ」という意味で、/ のようなルートパスでよく使います。付けないと、すべての下位パスでもアクティブになります。
React Router vs Next.js App Router — 意思決定 #
本書の4部は Next.js App Router を扱います。本章の React Router と同じ問題(URL に応じた画面遷移)を別の方法で解く道具です。2つの動作方式を短く比較しておきます。
| 項目 | React Router (v7) | Next.js App Router |
|---|---|---|
| ルート定義 | <Route path> のコンポーネントツリー | ファイルシステム(app/users/[userId]/page.tsx) |
| 動的パラメータ | :userId + useParams | [userId] フォルダ + params prop |
| レイアウト | <Outlet /> + ネストルート | app/layout.tsx(自動でネスト) |
| データフェッチ | コンポーネント内の useEffect(または v7 の loader API) | Server Component 関数本体で直接 fetch |
| SSR | オプション(Framework Mode の追加セットアップが必要) | デフォルト |
| ビルド結果 | クライアント SPA | RSC + クライアントコンポーネントの混在 |
| 学習コスト | 比較的シンプル | App Router + RSC モデルの学習が必要 |
| 適合する場面 | 素早く作る SPA、クライアントだけで動く道具 / ダッシュボード | SEO が重要なサービス、フルスタックアプリ、server-first モデル |
本章の React Router は クライアントサイド SPA に適しています。SEO の要求が低く、サーバーのデータフェッチが単純で、SPA を1つ素早く立ち上げればよいケースです。Next.js App Router は フルスタックアプリと SEO が重要なサービス に適しています。
本書の6部(フルスタック Todo capstone)は Next.js で作ります。この章の React Router は「React だけで SPA のルーティングをどう扱うか」の土台を作る段階です。
やってみよう #
ここまで学んだことを総合して、小さなミニサイトを作ってみます。ホーム、概要、ユーザー一覧、ユーザー詳細の4ページがあります。
src/Layout.jsx:
import { NavLink, Outlet } from 'react-router';
function Layout() {
const linkStyle = ({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
color: isActive ? 'tomato' : 'inherit',
marginRight: '12px',
});
return (
<div>
<header style={{ padding: '12px', background: '#f4f4f4', borderBottom: '1px solid #ccc' }}>
<NavLink to="/" end style={linkStyle}>ホーム</NavLink>
<NavLink to="/about" style={linkStyle}>概要</NavLink>
<NavLink to="/users" style={linkStyle}>ユーザー</NavLink>
</header>
<main style={{ padding: '16px' }}>
<Outlet />
</main>
</div>
);
}
export default Layout;src/pages/Home.jsx:
function Home() {
return (
<div>
<h1>ホーム</h1>
<p>React Router ミニサイトです。</p>
</div>
);
}
export default Home;src/pages/UserList.jsx:
import { Link } from 'react-router';
const USERS = [
{ id: 1, name: 'Cheolsu' },
{ id: 2, name: 'Younghee' },
{ id: 3, name: 'Minsu' },
];
function UserList() {
return (
<div>
<h1>ユーザー一覧</h1>
<ul>
{USERS.map(user => (
<li key={user.id}>
<Link to={`/users/${user.id}`}>{user.name}</Link>
</li>
))}
</ul>
</div>
);
}
export default UserList;src/pages/UserDetail.jsx:
import { useParams, useNavigate } from 'react-router';
const USERS = {
1: { name: 'Cheolsu', email: 'cheolsu@example.com' },
2: { name: 'Younghee', email: 'younghee@example.com' },
3: { name: 'Minsu', email: 'minsu@example.com' },
};
function UserDetail() {
const { userId } = useParams();
const navigate = useNavigate();
const user = USERS[userId];
if (!user) {
return (
<div>
<h1>ユーザーが見つかりません。</h1>
<button onClick={() => navigate('/users')}>一覧へ</button>
</div>
);
}
return (
<div>
<h1>{user.name}</h1>
<p>メール: {user.email}</p>
<button onClick={() => navigate(-1)}>戻る</button>
</div>
);
}
export default UserDetail;src/pages/NotFound.jsx:
import { Link } from 'react-router';
function NotFound() {
return (
<div>
<h1>404 — ページが見つかりません</h1>
<Link to="/">ホームへ戻る</Link>
</div>
);
}
export default NotFound;src/App.jsx:
import { BrowserRouter, Routes, Route } from 'react-router';
import Layout from './Layout';
import Home from './pages/Home';
import UserList from './pages/UserList';
import UserDetail from './pages/UserDetail';
import NotFound from './pages/NotFound';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="users" element={<UserList />} />
<Route path="users/:userId" element={<UserDetail />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
);
}
export default App;保存してブラウザで確認してみてください。
- ヘッダーのメニューをクリックすると、リロードなしで画面が変わります
- 現在のページのリンクは太字のトマト色
- ユーザー一覧で名前をクリック → 動的 URL(
/users/1)へ移動 → 詳細ページ - 「戻る」ボタンを押すとブラウザの戻る動作
- アドレスバーに
/nonexistentpathを直接入力すると 404 ページ
ここまで本書の1〜2部で学んだほぼすべて(コンポーネント分割、props、useState、イベント処理、条件付き / リストレンダリング)が1つのサイトに入っています。
練習問題 #
- 上のミニサイトに検索機能を追加してみてください。
/usersページに検索ボックスを置き、入力値をuseSearchParamsで URL に?q=...の形で反映します。リロードや URL の共有でも検索状態が復元されるか確認します。 - 保護されたルートを作る。
useStateでisLoggedInを扱うシンプルな認証フローを作り、/adminルートにアクセスするときにisLoggedIn === falseなら自動で/loginにリダイレクト(<Navigate to="/login" />)するようにしてみてください。32章(認証とセッション)の土台になります。 - React Router と Next.js App Router の比較。上のミニサイトを頭の中で Next.js App Router に移すなら、
app/ディレクトリの構造がどうなるか短く書いてみてください。例:app/layout.tsx、app/page.tsx、app/users/page.tsx、app/users/[userId]/page.tsx。4部の22〜23章に入る前に一度描いておくと役に立ちます。
一行まとめ: SPA は画面遷移をクライアントで処理する。React Router の核心は
BrowserRouter、Routes、Route、Link/NavLink。動的パスは:param+useParams、プログラマティックな移動はuseNavigate、クエリパラメータはuseSearchParams、共通レイアウトはネストルート +<Outlet />、404 はpath="*"。SEO が重要でフルスタックなら4部の Next.js App Router のほうが適している。
次の章 #
本章で 2部 エフェクト・状態・ルーティング が終わります。1部のコンポーネント / props / state / イベント / フォームに加えて、2部で useEffect / 状態のリフトアップ / Context / カスタムフック / パフォーマンス / ルーティングまで手に入れました。ライブラリなしに小さな SPA を最初から最後まで作る道具を揃えたことになります。
次の 16章 TypeScript + React のセットアップから3部が始まります。ここまでのすべてのコードを TypeScript の上に乗せ直します。props・フック・イベント・フォーム・Context・API レスポンスの型付けを6章にわたって順に扱います。