目次
15 章

ルーティング概要(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 です。

注記
Next.js のようなメタフレームワークはファイルシステムベースのルーティングを自前で提供するので、React Router を別途使いません。4部で扱います。本章では 純粋な React(Vite で作ったアプリ)で画面遷移 をどう扱うかに集中します。

React Router のインストール #

ここまで使ってきた Vite プロジェクトに React Router を追加します。

React Router のインストール
pnpm add react-router

本書は React Router v7 をベースにします。v7 から単一の react-router パッケージに統合され、古い資料でよく見る react-router-dom はもう別パッケージではありません。古いコードを移行するときは import だけ react-router-domreact-router に変えれば API はほぼそのまま動きます。

もっともシンプルな例 #

ルーティングの基本構造をまず見てみます。

src/App.jsx
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 の一部が動的に変わるパスがあります。コロン(:)を付けて動的な部分を示します。

src/App.jsx
<Route path="/users/:userId" element={<UserProfile />} />

/users/123/users/cheolsu のような URL はすべてこのルートにマッチします。コンポーネントの中では useParams フックで動的部分の値を取り出します。

src/UserProfile.jsx
import { useParams } from 'react-router';

function UserProfile() {
  const { userId } = useParams();

  return <h1>ユーザー ID: {userId}</h1>;
}

export default UserProfile;

useParamspath に明示したパラメータをすべてオブジェクトで返します。path="/users/:userId/posts/:postId" なら { userId, postId } を取り出せます。

プログラマティックなナビゲーション — useNavigate #

リンク以外に、コードから直接移動させたいときがあります。フォーム送信後に結果ページへ移動したり、ログアウトボタンがホームへ送ったりするケースです。useNavigate フックを使います。

src/LoginForm.jsx
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 を使います。

src/SearchPage.jsx
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>
  );
}

useSearchParamsuseState に似たインターフェイスで動作します。入力に応じて URL 自体が /search?q=react のように更新され、リロードや URL の共有でも同じ状態が復元されます。検索結果ページのように「URL に状態が反映されるべき」ケースに便利です。

ネストされたルートと Outlet #

複数のページが同じレイアウト(ヘッダー、サイドバーなど)を共有するときは、ネストされたルート がきれいです。親のルートが共通レイアウトを描き、子のルートがその中のコンテンツ領域を埋める構造です。

src/App.jsx
<Routes>
  <Route path="/" element={<Layout />}>
    <Route index element={<Home />} />
    <Route path="about" element={<About />} />
    <Route path="users/:userId" element={<UserProfile />} />
  </Route>
</Routes>
src/Layout.jsx
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> を使います。

src/Nav.jsx
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 の追加セットアップが必要)デフォルト
ビルド結果クライアント SPARSC + クライアントコンポーネントの混在
学習コスト比較的シンプル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:

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:

src/pages/Home.jsx
function Home() {
  return (
    <div>
      <h1>ホーム</h1>
      <p>React Router ミニサイトです</p>
    </div>
  );
}

export default Home;

src/pages/UserList.jsx:

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:

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:

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:

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つのサイトに入っています。

練習問題 #

  1. 上のミニサイトに検索機能を追加してみてください。/users ページに検索ボックスを置き、入力値を useSearchParams で URL に ?q=... の形で反映します。リロードや URL の共有でも検索状態が復元されるか確認します。
  2. 保護されたルートを作る。useStateisLoggedIn を扱うシンプルな認証フローを作り、/admin ルートにアクセスするときに isLoggedIn === false なら自動で /login にリダイレクト(<Navigate to="/login" />)するようにしてみてください。32章(認証とセッション)の土台になります。
  3. React Router と Next.js App Router の比較。上のミニサイトを頭の中で Next.js App Router に移すなら、app/ ディレクトリの構造がどうなるか短く書いてみてください。例:app/layout.tsxapp/page.tsxapp/users/page.tsxapp/users/[userId]/page.tsx。4部の22〜23章に入る前に一度描いておくと役に立ちます。

一行まとめ: SPA は画面遷移をクライアントで処理する。React Router の核心は BrowserRouterRoutesRouteLink / 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章にわたって順に扱います。

X