React

React Router v6実践ガイド|SPAのルーティングをマスターする

React Router SPA

React Router v6実践ガイド
SPA のルーティングをマスターする

React Router v6のネストルート、動的パラメータ、認証ガード、遅延読み込みまで、SPA開発に必要なルーティングテクニックを解説します。

こんな人向けの記事です

  • React Router v6の使い方を学びたい
  • ネストルートやプロテクテッドルートを実装したい
  • パフォーマンスを考慮した遅延読み込みを導入したい

Step 1React Router v6の基本セットアップ

React Router v6は、React SPAでページ遷移を実現するための標準的なライブラリです。v5から大幅にAPIが刷新され、よりシンプルで直感的な設計になりました。

インストール

ターミナル
npm install react-router-dom@6

基本構造 — BrowserRouter, Routes, Route

React Router v6の基本は3つのコンポーネントで構成されます。

コンポーネント役割
BrowserRouterHTML5 History APIを使用したルーター。アプリ全体をラップする
Routesルート定義のコンテナ。最初にマッチしたルートのみレンダリング
Routeパスとコンポーネントの対応を定義
main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);
App.jsx
import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import NotFound from './pages/NotFound';

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

export default App;
v5からの主な変更点
  • SwitchRoutes に名称変更
  • component / render props → element prop(JSXを直接渡す)
  • exact prop は不要(v6はデフォルトで完全一致)
  • ルートの優先順位は定義順ではなく最も具体的なパスが自動で選ばれる

ナビゲーション — Link と NavLink

components/Navigation.jsx
import { Link, NavLink } from 'react-router-dom';

function Navigation() {
  return (
    <nav>
      {/* 基本のリンク */}
      <Link to="/">ホーム</Link>

      {/* アクティブ状態を自動判定するリンク */}
      <NavLink
        to="/about"
        className={({ isActive }) =>
          isActive ? 'nav-link active' : 'nav-link'
        }
      >
        About
      </NavLink>
    </nav>
  );
}
注意: 通常の<a>タグを使うとページ全体がリロードされ、SPAの利点が失われます。ページ内遷移には必ずLinkまたはNavLinkを使用してください。

Step 2ネストされたルート(Outlet)

React Router v6の最大の特徴の一つが、ネストルートです。親ルートの中に子ルートを定義し、Outletコンポーネントで子の表示位置を制御します。

基本的なネストルート

App.jsx — ネストルートの定義
import { Routes, Route } from 'react-router-dom';
import Layout from './components/Layout';
import Home from './pages/Home';
import Dashboard from './pages/Dashboard';
import DashboardOverview from './pages/DashboardOverview';
import DashboardSettings from './pages/DashboardSettings';
import DashboardProfile from './pages/DashboardProfile';

function App() {
  return (
    <Routes>
      {/* 共通レイアウトを親ルートにする */}
      <Route path="/" element={<Layout />}>
        <Route index element={<Home />} />

        {/* /dashboard 以下をネスト */}
        <Route path="dashboard" element={<Dashboard />}>
          <Route index element={<DashboardOverview />} />
          <Route path="settings" element={<DashboardSettings />} />
          <Route path="profile" element={<DashboardProfile />} />
        </Route>
      </Route>
    </Routes>
  );
}

Outletで子ルートを表示する

components/Layout.jsx — 共通レイアウト
import { Outlet } from 'react-router-dom';
import Navigation from './Navigation';
import Footer from './Footer';

function Layout() {
  return (
    <div className="app">
      <Navigation />
      <main>
        {/* 子ルートのコンポーネントがここに表示される */}
        <Outlet />
      </main>
      <Footer />
    </div>
  );
}
pages/Dashboard.jsx — ダッシュボードレイアウト
import { Outlet, NavLink } from 'react-router-dom';

function Dashboard() {
  return (
    <div className="dashboard">
      <aside className="sidebar">
        <NavLink to="/dashboard" end>概要</NavLink>
        <NavLink to="/dashboard/settings">設定</NavLink>
        <NavLink to="/dashboard/profile">プロフィール</NavLink>
      </aside>
      <section className="content">
        {/* Dashboard配下の子ルートがここに表示 */}
        <Outlet />
      </section>
    </div>
  );
}

上記の構成により、URL に応じて以下のように表示されます。

URLLayoutDashboard表示される子
/表示Home
/dashboard表示表示DashboardOverview
/dashboard/settings表示表示DashboardSettings
/dashboard/profile表示表示DashboardProfile
index ルートとは
<Route index element={...} /> は、親ルートのパスにぴったり一致した時に表示されるデフォルトの子ルートです。path の代わりに index を指定します。

Step 3useNavigate / useParams / useSearchParams

React Router v6では、ルーティングに関する操作をHooksで行います。主要な3つのHooksを見ていきます。

useNavigate — プログラムによる遷移

v5のuseHistoryに代わる新しいHookです。

pages/LoginForm.jsx
import { useNavigate } from 'react-router-dom';

function LoginForm() {
  const navigate = useNavigate();

  const handleLogin = async (e) => {
    e.preventDefault();
    const result = await loginAPI(email, password);

    if (result.success) {
      // ダッシュボードに遷移(履歴に残る)
      navigate('/dashboard');

      // 履歴を置き換える場合(戻るボタンでログイン画面に戻れなくなる)
      // navigate('/dashboard', { replace: true });
    }
  };

  return (
    <form onSubmit={handleLogin}>
      {/* フォーム内容 */}
      <button type="submit">ログイン</button>

      {/* 1つ前のページに戻る */}
      <button type="button" onClick={() => navigate(-1)}>
        戻る
      </button>
    </form>
  );
}
呼び出し方動作
navigate('/path')指定パスに遷移
navigate('/path', { replace: true })履歴を置き換えて遷移
navigate(-1)1つ前に戻る(ブラウザバック)
navigate(1)1つ先に進む
navigate('/path', { state: { from: 'login' } })state付きで遷移

useParams — URLパラメータの取得

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

// Route定義: <Route path="/users/:userId" element={<UserProfile />} />

function UserProfile() {
  // URL /users/42 にアクセスした場合 → userId = "42"
  const { userId } = useParams();

  return <h1>ユーザー #{userId} のプロフィール</h1>;
}
注意: useParams() の値は常に文字列です。数値として使用する場合は Number(userId) で変換してください。

useSearchParams — クエリパラメータの操作

pages/ProductList.jsx
import { useSearchParams } from 'react-router-dom';

// URL例: /products?category=electronics&sort=price

function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();

  // クエリパラメータの取得
  const category = searchParams.get('category'); // "electronics"
  const sort = searchParams.get('sort');         // "price"
  const page = searchParams.get('page') || '1';  // デフォルト値

  // クエリパラメータの更新
  const handleCategoryChange = (newCategory) => {
    setSearchParams({
      category: newCategory,
      sort: sort || 'name',
      page: '1', // カテゴリ変更時はページをリセット
    });
  };

  // 既存パラメータを保持しつつ一部を更新
  const handlePageChange = (newPage) => {
    setSearchParams((prev) => {
      prev.set('page', String(newPage));
      return prev;
    });
  };

  return (
    <div>
      <h1>商品一覧({category})</h1>
      <select
        value={category || ''}
        onChange={(e) => handleCategoryChange(e.target.value)}
      >
        <option value="">すべて</option>
        <option value="electronics">電子機器</option>
        <option value="clothing">衣類</option>
      </select>
      {/* 商品リストとページネーション */}
    </div>
  );
}
useSearchParams の特徴
useSearchParamsuseState に似たAPIです。searchParamsURLSearchParams オブジェクトなので、.get(), .getAll(), .has() などのメソッドが使えます。

Step 4動的ルーティングとパラメータ

実際のアプリケーションでは、商品詳細やブログ記事など、動的なデータに基づいたルーティングが必要です。

複数パラメータの動的ルート

App.jsx — 動的ルート定義
function App() {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        {/* 商品カテゴリ → 商品詳細 */}
        <Route path="products" element={<ProductLayout />}>
          <Route index element={<ProductList />} />
          <Route path=":category" element={<CategoryPage />} />
          <Route path=":category/:productId" element={<ProductDetail />} />
        </Route>

        {/* ブログ — 年/月/スラッグ */}
        <Route path="blog" element={<BlogLayout />}>
          <Route index element={<BlogList />} />
          <Route path=":year/:month/:slug" element={<BlogPost />} />
        </Route>
      </Route>
    </Routes>
  );
}

パラメータのバリデーションとデータ取得

pages/ProductDetail.jsx
import { useParams, useNavigate } from 'react-router-dom';
import { useState, useEffect } from 'react';

function ProductDetail() {
  const { category, productId } = useParams();
  const navigate = useNavigate();
  const [product, setProduct] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchProduct = async () => {
      try {
        setLoading(true);
        const res = await fetch(`/api/products/${category}/${productId}`);

        if (!res.ok) {
          if (res.status === 404) {
            // 商品が見つからない場合は404ページに遷移
            navigate('/not-found', { replace: true });
            return;
          }
          throw new Error('データの取得に失敗しました');
        }

        const data = await res.json();
        setProduct(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchProduct();
  }, [category, productId, navigate]);

  if (loading) return <div>読み込み中...</div>;
  if (error) return <div>エラー: {error}</div>;
  if (!product) return null;

  return (
    <div>
      <h1>{product.name}</h1>
      <p>カテゴリ: {category}</p>
      <p>価格: ¥{product.price.toLocaleString()}</p>
    </div>
  );
}

オプショナルパラメータとワイルドカード

App.jsx — 特殊なルートパターン
<Routes>
  {/* ワイルドカード — /files 以下の全パスにマッチ */}
  <Route path="/files/*" element={<FileExplorer />} />

  {/* オプショナルパラメータは複数Routeで対応 */}
  <Route path="/users" element={<UserList />} />
  <Route path="/users/:userId" element={<UserDetail />} />

  {/* 404 — 全てにマッチしなかった場合 */}
  <Route path="*" element={<NotFound />} />
</Routes>
pages/FileExplorer.jsx — ワイルドカードの取得
import { useParams } from 'react-router-dom';

// URL /files/documents/2024/report.pdf の場合
function FileExplorer() {
  const { '*': filePath } = useParams();
  // filePath = "documents/2024/report.pdf"

  return <div>ファイルパス: {filePath}</div>;
}
v6のルートマッチングの賢さ
React Router v6はルートランキングを自動で行います。例えば /users/new/users/:userId を同時に定義しても、/users/new にアクセスすると静的なパス(/users/new)が優先されます。定義順に依存しないため、安心してルートを追加できます。

Step 5プロテクテッドルート(認証ガード)

認証が必要なページを保護するために、ラッパーコンポーネントを作成します。未認証のユーザーをログインページにリダイレクトするパターンです。

認証コンテキストの作成

contexts/AuthContext.jsx
import { createContext, useContext, useState } from 'react';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);

  const login = async (email, password) => {
    const res = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });
    const data = await res.json();
    if (data.user) {
      setUser(data.user);
      return { success: true };
    }
    return { success: false, message: data.message };
  };

  const logout = () => {
    setUser(null);
  };

  return (
    <AuthContext.Provider value={{ user, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export const useAuth = () => useContext(AuthContext);

ProtectedRoute コンポーネント

components/ProtectedRoute.jsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';

function ProtectedRoute({ children }) {
  const { user } = useAuth();
  const location = useLocation();

  if (!user) {
    // 未認証 → ログインページにリダイレクト
    // state に現在のパスを保存(ログイン後に戻れるように)
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return children;
}
components/RoleGuard.jsx — ロールベースの認可
import { Navigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';

function RoleGuard({ children, allowedRoles }) {
  const { user } = useAuth();

  if (!user) {
    return <Navigate to="/login" replace />;
  }

  if (!allowedRoles.includes(user.role)) {
    return <Navigate to="/unauthorized" replace />;
  }

  return children;
}

ルート定義への適用

App.jsx — プロテクテッドルートの適用
import { Routes, Route } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
import ProtectedRoute from './components/ProtectedRoute';
import RoleGuard from './components/RoleGuard';

function App() {
  return (
    <AuthProvider>
      <Routes>
        {/* 公開ページ */}
        <Route path="/" element={<Home />} />
        <Route path="/login" element={<Login />} />

        {/* 認証が必要なページ */}
        <Route
          path="/dashboard"
          element={
            <ProtectedRoute>
              <Dashboard />
            </ProtectedRoute>
          }
        />

        {/* 管理者のみアクセス可能 */}
        <Route
          path="/admin/*"
          element={
            <RoleGuard allowedRoles={['admin']}>
              <AdminPanel />
            </RoleGuard>
          }
        />
      </Routes>
    </AuthProvider>
  );
}

ログイン後のリダイレクト

pages/Login.jsx — ログイン後に元のページへ戻る
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';

function Login() {
  const navigate = useNavigate();
  const location = useLocation();
  const { login } = useAuth();

  // リダイレクト元のパスを取得(なければダッシュボード)
  const from = location.state?.from?.pathname || '/dashboard';

  const handleSubmit = async (e) => {
    e.preventDefault();
    const result = await login(email, password);

    if (result.success) {
      // ログイン前にアクセスしようとしたページに遷移
      navigate(from, { replace: true });
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* フォーム内容 */}
    </form>
  );
}
注意: ProtectedRoute はクライアントサイドの保護のみです。APIエンドポイント側でも必ず認証・認可チェックを行ってください。クライアントサイドのガードはUX向上のためであり、セキュリティの最終防衛線ではありません。

Step 6lazy loading(React.lazy + Suspense)

SPAではルートごとにコンポーネントを遅延読み込み(lazy loading)することで、初期バンドルサイズを削減し、パフォーマンスを大幅に改善できます。

基本的な遅延読み込み

App.jsx — React.lazy + Suspense
import React, { Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

// React.lazy で動的インポート
const Home = React.lazy(() => import('./pages/Home'));
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Settings = React.lazy(() => import('./pages/Settings'));
const AdminPanel = React.lazy(() => import('./pages/AdminPanel'));

// ローディングコンポーネント
function LoadingFallback() {
  return (
    <div className="loading">
      <div className="spinner" />
      <p>読み込み中...</p>
    </div>
  );
}

function App() {
  return (
    <Suspense fallback={<LoadingFallback />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/admin/*" element={<AdminPanel />} />
      </Routes>
    </Suspense>
  );
}

ルートごとの Suspense 分離

全体を1つのSuspenseで囲むと、どのページに遷移してもフォールバックUIが表示されます。ルートごとにSuspenseを分離すると、遷移元のページを表示したままローディングできます。

components/LazyRoute.jsx — 再利用可能なラッパー
import { Suspense } from 'react';

function LazyRoute({ component: Component, fallback }) {
  return (
    <Suspense fallback={fallback || <LoadingFallback />}>
      <Component />
    </Suspense>
  );
}

// 使用例
const Dashboard = React.lazy(() => import('./pages/Dashboard'));

<Route
  path="/dashboard"
  element={<LazyRoute component={Dashboard} />}
/>

プリフェッチで体感速度を向上させる

utils/lazyWithPreload.js — プリフェッチ対応の lazy
// プリロード機能付きの lazy wrapper
export function lazyWithPreload(factory) {
  const Component = React.lazy(factory);
  Component.preload = factory;
  return Component;
}

// 使用例
const Dashboard = lazyWithPreload(
  () => import('./pages/Dashboard')
);
components/Navigation.jsx — ホバー時にプリフェッチ
import { Link } from 'react-router-dom';

// Dashboard ページをホバー時にプリロード
function Navigation() {
  return (
    <nav>
      <Link to="/">ホーム</Link>
      <Link
        to="/dashboard"
        onMouseEnter={() => Dashboard.preload()}
      >
        ダッシュボード
      </Link>
    </nav>
  );
}
lazy loading の効果
  • 初期バンドルサイズの削減 — ユーザーが最初に訪れるページのコードのみ読み込む
  • ルート単位のコード分割 — Webpack/Viteが自動的にチャンクを生成
  • プリフェッチとの組み合わせ — ユーザーが遷移する前にコードを先読みして体感速度を向上
注意: React.lazyデフォルトエクスポートのみサポートします。名前付きエクスポートを使用する場合は中間モジュールを作成するか、import('./Module').then(m => ({ default: m.NamedExport })) のように変換してください。

まとめチェックリスト

  • BrowserRouter でアプリ全体をラップし、Routes + Route でルートを定義できる
  • ネストルートと Outlet で共通レイアウトを再利用できる
  • useNavigate でプログラム遷移、useParams でURLパラメータ取得、useSearchParams でクエリ操作ができる
  • 動的ルーティングとワイルドカードルートを使いこなせる
  • ProtectedRoute パターンで認証ガードを実装できる
  • React.lazy + Suspense でルート単位の遅延読み込みを実装できる