React基礎講座 #15 ルーティング概要 (React Router)

読了 11分

前回はパフォーマンス最適化ツールを扱いました。今回はこのシリーズの最後の記事です。これまで私たちは1つの画面の中で起きることを扱ってきましたが、実際のアプリは普通複数の画面を持ちます。メニュークリックに応じて画面が変わり、URLが変わり、戻るボタンも動作しなければなりません。こうした画面遷移を扱うツールがルーティングです。

伝統的なウェブ vs SPA #

伝統的なウェブページはユーザーがリンクをクリックするたびにブラウザがサーバーに新しいページをリクエストし、サーバーが作った新しいHTMLを受け取って画面全体を再描画していました。ページ遷移ごとに白いちらつきが起きるあの方式です。

SPA(Single Page Application) は最初に1回HTMLを受け取った後、その後の画面遷移はJavaScriptで画面を再描画する方式です。サーバーに新しいHTMLをリクエストせずクライアントが勝手に画面を入れ替えるので、遷移が速くてスムーズです。

Reactで作ったアプリは基本的にSPAです。ところがSPAは最初に受け取ったそのHTMLの中ですべてのことが起きるので、「URLが変わったらどの画面を見せるか」を私たちが直接決めてあげる必要があります。これをクライアントサイドルーティングと呼び、Reactエコシステムの事実上の標準ライブラリがReact Routerです。

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

React Routerのインストール #

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

React Routerのインストール
npm install react-router-dom

(react-routerではなくreact-router-domです — ウェブ用のReact Routerパッケージ)

もっとも単純な例 #

ルーティングの基本構造をまずお見せします。

src/App.jsx
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

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と一致する1つを選んでレンダリングするコンテナ
  • <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-dom';

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-dom';

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-dom';

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-dom';

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に応じて中央のコンテンツだけ変えられます。

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-dom';

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は「正確にこのパスのときだけアクティブ」という意味で、/のようなルートパスでよく使います(付けないとすべての下位パスでもアクティブとして判定されます)。

自分でやってみる #

これまで学んだことを総合して小さなミニサイトを作ってみましょう。ホーム、紹介、ユーザー一覧、ユーザー詳細の4ページがあります。

src/Layout.jsx:

src/Layout.jsx
import { NavLink, Outlet } from 'react-router-dom';

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/About.jsx:

src/pages/About.jsx
function About() {
  return (
    <div>
      <h1>紹介</h1>
      <p>このシリーズの最終記事で作った例です</p>
    </div>
  );
}

export default About;

src/pages/UserList.jsx:

src/pages/UserList.jsx
import { Link } from 'react-router-dom';

const USERS = [
  { id: 1, name: '太郎' },
  { id: 2, name: '花子' },
  { id: 3, name: '次郎' },
];

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-dom';

const USERS = {
  1: { name: '太郎', email: 'taro@example.com' },
  2: { name: '花子', email: 'hanako@example.com' },
  3: { name: '次郎', email: 'jiro@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-dom';

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-dom';
import Layout from './Layout';
import Home from './pages/Home';
import About from './pages/About';
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="about" element={<About />} />
          <Route path="users" element={<UserList />} />
          <Route path="users/:userId" element={<UserDetail />} />
          <Route path="*" element={<NotFound />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

export default App;

保存してブラウザで確認してみてください。

  • ヘッダーメニューをクリックするとリロードなしに画面が変わります
  • 現在のページのリンクは太字でトマト色
  • ユーザー一覧で名前をクリック → 動的URL(/users/1)に移動 → 詳細ページ
  • 「戻る」ボタンを押すとブラウザの履歴を戻ります
  • アドレスバーに/non-existent-pathを直接入力 → 404ページ

これまでシリーズを通して学んだほぼすべてが1つのサイトに入っています。コンポーネント分離、props、useState、イベント処理、条件付き/リストレンダリングまでです。

おわりに — そしてシリーズを終えるにあたって #

今回の記事ではルーティングを扱いました。まとめると:

  • SPAは画面遷移をクライアントで処理する → ルーティングライブラリが必要
  • React Routerの核心: BrowserRouterRoutesRouteLink/NavLink
  • 動的経路(:param)とuseParams
  • プログラマティック移動: useNavigate
  • クエリパラメータ: useSearchParams
  • 共通レイアウト: ネストルート + <Outlet />
  • path="*"で404処理

これでReact基礎講座シリーズ(#1〜#15) がすべて終わりました。最初に「Reactって何?」から始めて、今ではルーティングのある小さなSPAを自分で作れるところまで来ました。シリーズで扱ったことを一度振り返ると:

  • #1〜3 基礎をしっかり — Reactの正体、環境設定、JSX
  • #4〜8 核心ビルディングブロック — コンポーネント/props、state、イベント、条件付き/リストレンダリング
  • #9〜12 実戦パターン — フォーム、useEffect、stateのリフトアップ、Context
  • #13〜15 締めくくり — カスタムフック、パフォーマンス最適化、ルーティング

ここで学んだ内容はどんなReactベースのフレームワーク(Next.js、Remixなど)を使っても同じく通じるファンダメンタルです。フレームワークはその上にルーティング、データフェッチ、SSRのような付加機能を乗せたものに過ぎません。

次のステップに進む方におすすめする道:

  • モダンReact 19 + Next.jsシリーズ(予定) — Server Components、use()、Actions、Suspenseのような最新モデル
  • 実戦ビルドシリーズ(予定) — Todoアプリ、ブログ、ショッピングモールのような小さなプロジェクトで総合練習
  • 自分で作りたい小さなプロジェクト(個人メモ帳、運動記録など)を始めて、詰まるたびに公式ドキュメントを探して読む

ここまで読んでくださってありがとうございます。小さなコンポーネント1つ1つが集まって大きなアプリになるように、1つの記事1つの記事を着実についてきてくださった方ならすでにご自身の最初のReactアプリを十分作れる方でしょう。楽しいReactの旅になりますように!

X