404 Not Found

404 Not Found


nginx

React Router:応用編

トムは基本的なルーティングの実装に成功しましたが、新たな要件が次々と浮上しています。具体的には、ユーザーがログイン後に自動的にダッシュボードにリダイレクトされる必要があること、特定のページにアクセスするにはログインが必要であること、アプリの規模拡大に伴いホームページの読み込みに時間がかかるようになったこと、そして商品一覧ページでURL内のフィルタパラメータに対応する必要があることなどです……。こうした実世界のシナリオに対処するためには、React Routerの高度な機能を学ぶ必要があります。


1. 学習内容


2. 概念図

100%
flowchart TD
    U[User Actions] --> A{Are you logged in??}
    A -->|Not logged in| B[ProtectedRoute<br/>→ Redirect to /login]
    A -->|Logged in| C{Permissions?}
    C -->|No permission| D[→ 403 Page]
    C -->|Has permission| E[Load the target page]
    E --> F{Is the component lazily loaded??}
    F -->|is | G[Suspense<br/>Show loading]
    G --> H[Rendering Page]
    F -->|No| H
    style A fill:#fff3e0,stroke:#f57c00
    style B fill:#ffcdd2,stroke:#d32f2f
    style G fill:#e1f5fe,stroke:#0288d1
    style H fill:#e8f5e9,stroke:#388e3c

ユーザーのリクエスト → ルートガードによるログイン・権限の確認 → 承認後にページが読み込まれる(レイジーローディング中は「読み込み中」と表示される) → 最終的なレンダリング。


3. 実際の事例

トムのアプリには、以下の要件があります。ログインに成功すると、ユーザーは自動的にダッシュボードにリダイレクトされること。ダッシュボードページはログイン認証で保護されていること。「admin」ロールのユーザーのみが管理パネルにアクセスできること。ホームページは最初の画面で素早く読み込まれること(大容量のページは必要に応じて読み込まれる)。また、製品一覧ページのURLには、共有を容易にするためにフィルタリングおよびページネーションのパラメータが含まれていること。

(1) プログラムによるナビゲーション:useNavigate

トムは、ログインページでのフォーム送信を処理し、送信が成功した際には、ユーザーの役割に応じて異なるページへリダイレクトする必要があります。コードのロジックによってトリガーされるこの種のナビゲーションでは、<Link> を使用することはできません(リンクはユーザーのクリックによってのみトリガーされるため)。その代わりに、useNavigate を使用する必要があります。

useNavigate は、3つの呼び出し方法をサポートする navigate 関数を返します:

使い方 効果
navigate('/path') 指定したパスへ移動(履歴スタックに新しいエントリを追加) navigate('/dashboard')
navigate('/path', { replace: true }) 現在の履歴を上書き(この時点には戻れません) ログイン後にリダイレクト
navigate(-1) 戻る 前のページに戻る

(1) ▶ サンプル:ログイン後のプログラムによるナビゲーション

JSX
import { useNavigate, useLocation } from 'react-router-dom'

function LoginPage() {
  const navigate = useNavigate()
  const location = useLocation()

  // from  URL Retrieve the URL to redirect to after login from the query parameters
  const from = location.state?.from?.pathname || '/dashboard'

  function handleLogin(event) {
    event.preventDefault()
    const formData = new FormData(event.target)
    const username = formData.get('username')

    // Simulate a login request
    fakeLogin(username).then(user => {
      if (user.role === 'admin') {
        navigate('/admin', { replace: true })     // Replace Record,Cannot go back to the login page
      } else {
        navigate(from, { replace: true })          // Redirect to the page you were trying to access before logging in
      }
    })
  }

  // Login Form
  return (
    <form onSubmit={handleLogin}>
      <input name="username" placeholder="Username" required />
      <button type="submit">Log In</button>
      <button type="button" onClick={() => navigate(-1)}>Back</button>
    </form>
  )
}

// Simulated Login API
async function fakeLogin(username) {
  await new Promise(r => setTimeout(r, 500))
  return { name: username, role: username === 'admin' ? 'admin' : 'user' }
}
▶ 試してみよう

useLocationの役割: location.state Linkまたはnavigateから渡された状態データを受け取ることができます。例えば、ProtectedRoute内でログインページへリダイレクトする際、ユーザーが当初アクセスしようとしていたページのパスをstateに保存しておけば、ログイン後にそのページへ自動的にリダイレクトされるようになります。

(2) ルートガード:ProtectedRoute

ガードの種類 チェックロジック 失敗時の動作 代表的なシナリオ
ログインガード isAuthenticated /login にリダイレクト ダッシュボード、マイアカウント
権限ガード user.role === 'admin' /403 にリダイレクト 管理パネル
機能スイッチガード featureFlags.xEnabled /upgrade にリダイレクト 有料機能
条件付きガード profileComplete /onboarding へリダイレクト 初回ログインガイド

トムのダッシュボードページにアクセスするにはログインが必要であり、管理パネルには「admin」ロールが必要です。彼には、ページを表示する前にユーザーのステータスを確認し、条件を満たしていない場合はリダイレクトする「ガード」機能が必要です。

RouteGuardは、本質的にラッパーコンポーネントです。子コンポーネントを受け取り、レンダリング前に検証ロジックを実行します。検証に合格した場合は子コンポーネントをレンダリングし、不合格の場合は<Navigate>を使用して別のページにリダイレクトします。

(2) ▶ サンプル:多層ルーティングガード

JSX
import { Navigate, useLocation } from 'react-router-dom'

// Simulated Certification Hook
function useAuth() {
  return {
    user: { name: 'Tom', role: 'admin' },  // In actual projects, starting from Context or  store Get
    isAuthenticated: true
  }
}

// First Floor:Login Guard
function ProtectedRoute({ children }) {
  const { isAuthenticated } = useAuth()
  const location = useLocation()

  if (!isAuthenticated) {
    // Save the user's destination to state in ,Redirect back after logging in
    return <Navigate to="/login" state={{ from: location }} replace />
  }

  return children
}

// Second Floor:Character Guard
function AdminRoute({ children }) {
  const { user } = useAuth()

  if (user.role !== 'admin') {
    return <Navigate to="/403" replace />
  }

  return children
}

// Used in router configuration
function AppRoutes() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/login" element={<LoginPage />} />
      <Route path="/403" element={<AccessDenied />} />

      {/* You must log in */}
      <Route path="/dashboard" element={
        <ProtectedRoute>
          <Dashboard />
        </ProtectedRoute>
      } />

      {/* You must log in + admin Character */}
      <Route path="/admin" element={
        <ProtectedRoute>
          <AdminRoute>
            <AdminPanel />
          </AdminRoute>
        </ProtectedRoute>
      } />
    </Routes>
  )
}
▶ 試してみよう

ネストされたガードパターン: 外側の ProtectedRoute は「ユーザーがログインしているかどうか」を確認し、内側の AdminRoute は「ユーザーが管理者権限を持っているかどうか」を確認します。これにより、責任が分離され、再利用が可能になります。将来「編集」というロールが必要になった場合は、単に EditorRoute を追加するだけで済みます。

(3) 遅延読み込み:React.lazy + Suspense

アプリが成長するにつれ、トムはホームページの読み込みにますます時間がかかるようになったことに気づきました。すべてのページのコードが1つのバンドルにまとめられていたため、ユーザーがホームページにアクセスするたびに、管理パネルのコードもダウンロードしなければならなかったのです。React.lazy を使えば、コンポーネントのコードを個別のチャンクに分割し、必要なときにのみ読み込むことができます。

JSX
import { lazy, Suspense } from 'react'

// These components will not be bundled into the main bundle in 
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))
const AdminPanel = lazy(() => import('./pages/AdminPanel'))
const UserList = lazy(() => import('./pages/UserList'))

(3) ▶ サンプル:ルートレベルでの遅延読み込み

JSX
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'

// Lazy-load all page components
const Home = lazy(() => import('./pages/Home'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
const ProductList = lazy(() => import('./pages/ProductList'))
const ProductDetail = lazy(() => import('./pages/ProductDetail'))
const Settings = lazy(() => import('./pages/Settings'))
const NotFound = lazy(() => import('./pages/NotFound'))

// Loading component
function PageLoader() {
  return (
    <div style={{
      display: 'flex', justifyContent: 'center', alignItems: 'center',
      height: '100vh', fontSize: '1.2rem', color: '#666'
    }}>
      <div className="spinner" />
      <span style={{ marginLeft: '12px' }}>Page is loading...</span>
    </div>
  )
}

function App() {
  return (
    <BrowserRouter>
      {/* Suspense Wrap all lazy-loading routes */}
      <Suspense fallback={<PageLoader />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/products" element={<ProductList />} />
          <Route path="/products/:id" element={<ProductDetail />} />
          <Route path="/settings" element={<Settings />} />
          <Route path="*" element={<NotFound />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  )
}

export default App
▶ 試してみよう

レイジーローディングの仕組み: Reactは、ユーザーが/dashboardを初めて訪問したときにのみ、/dashboardチャンクを動的に読み込みます。読み込み中は、Suspenseのfallbackコンポーネントが表示されます。読み込みが完了すると、実際のDashboardコンポーネントに置き換えられます。

パフォーマンス上のメリット: 元のバンドルが 500 KB であると仮定すると、レイジーローディングによりメインバンドルは 100 KB に縮小され、各ページのサイズはおよそ 80 KB になります。ユーザーがホームページにアクセスする際、500 KB ではなく 100 KB のみをダウンロードすれば済むため、画面上部(アバブ・ザ・フォールド)の読み込み速度が約 5 倍向上します。


4. useSearchParams:URLクエリパラメータの管理

URLパラメータのメソッド API 構文例 適用可能なデータ
パスパラメータ useParams() /products/:id{ id: '42' } 必須の識別子(リソースID)
クエリパラメータ useSearchParams() ?sort=price&page=2 オプションのフィルタリング/ソート/ページング
ハッシュ useLocation().hash #section-3 ページ内リンク
状態 useLocation().state navigate('/path', { state }) ページ間で隠しデータを渡す

トムの商品一覧ページでは、カテゴリによるフィルタリング、価格順の並べ替え、ページネーションなどの機能をサポートする必要があります。また、ユーザーがフィルタリングしたリンクを他の人と共有できるように、これらのフィルタ条件がURLに反映されるようにする必要があります。

(1) クエリパラメータの読み取りと書き込み

useSearchParams は、Map と同様に URL のクエリパラメータの読み書きができる Map のようなオブジェクトを返します。useState と同様に、値とセッターを返します。

JSX
import { useSearchParams } from 'react-router-dom'

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

  const category = searchParams.get('category') || 'All'
  const sort = searchParams.get('sort') || 'default'
  const page = Number(searchParams.get('page')) || 1

  function updateFilter(key, value) {
    setSearchParams(prev => {
      if (value) prev.set(key, value)
      else prev.delete(key)
      return prev
    })
  }

  return (
    <div>
      <p>Current Category:{category} | Sort:{sort} | Page: {page}</p>
      <button onClick={() => updateFilter('category', 'Electronics')}>Electronics Categories</button>
      <button onClick={() => updateFilter('sort', 'price')}>Sort by Price</button>
      <button onClick={() => updateFilter('page', String(page + 1))}>Next Page</button>
      <button onClick={() => setSearchParams({})}>Clear Filters</button>
    </div>
  )
}
// URL It will be updated in real time:/products?category=Electronics&sort=price&page=2

(1) ▶ サンプル:製品フィルタリング機能の完成形

JSX
import { useSearchParams } from 'react-router-dom'

// Simulated Product Data
const allProducts = [
  { id: 1, name: 'iPhone 16', category: 'Electronics', price: 6999 },
  { id: 2, name: 'React Programming Books', category: 'Books', price: 79 },
  { id: 3, name: 'Mechanical Keyboard', category: 'Electronics', price: 399 },
  { id: 4, name: 'Introduction to Design Patterns', category: 'Books', price: 59 },
  { id: 5, name: 'Bluetooth Headphones', category: 'Electronics', price: 899 },
]

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

  // from  URL Read Filter Criteria
  const category = searchParams.get('category') || ''
  const sortBy = searchParams.get('sort') || 'name'
  const page = parseInt(searchParams.get('page') || '1', 10)
  const pageSize = 3

  // Filtering
  let filtered = category
    ? allProducts.filter(p => p.category === category)
    : allProducts

  // Sort
  if (sortBy === 'price') {
    filtered = [...filtered].sort((a, b) => a.price - b.price)
  } else {
    filtered = [...filtered].sort((a, b) => a.name.localeCompare(b.name))
  }

  // Pagination
  const totalPages = Math.ceil(filtered.length / pageSize)
  const paged = filtered.slice((page - 1) * pageSize, page * pageSize)

  // Update Filter Criteria
  function setFilter(key, value) {
    setSearchParams(prev => {
      const next = new URLSearchParams(prev)
      if (value) {
        next.set(key, value)
      } else {
        next.delete(key)
      }
      next.set('page', '1')  // Return to the first page when switching filters
      return next
    })
  }

  return (
    <div>
      <div style={{ marginBottom: '16px' }}>
        <label>Categories:
          <select value={category} onChange={e => setFilter('category', e.target.value)}>
            <option value="">All</option>
            <option value="Electronics">Electronics</option>
            <option value="Books">Books</option>
          </select>
        </label>
        <label style={{ marginLeft: '16px' }}>Sort:
          <select value={sortBy} onChange={e => setFilter('sort', e.target.value)}>
            <option value="name">Name</option>
            <option value="price">Price</option>
          </select>
        </label>
      </div>

      <ul>
        {paged.map(p => (
          <li key={p.id}>{p.name} — ${p.price}({p.category})</li>
        ))}
      </ul>

      <div>
        {Array.from({ length: totalPages }, (_, i) => (
          <button
            key={i}
            onClick={() => setSearchParams(prev => {
              const next = new URLSearchParams(prev)
              next.set('page', String(i + 1))
              return next
            })}
            style={{ fontWeight: page === i + 1 ? 'bold' : 'normal' }}
          >
            {i + 1}
          </button>
        ))}
      </div>

      <p>Currently URL:/products?category={category}&sort={sortBy}&page={page}</p>
    </div>
  )
}
▶ 試してみよう

キーワード: URLのクエリパラメータは「共有可能な状態」を表します。ユーザーがフィルタを適用し、そのURLをコピーして同僚に送信すると、同僚がそのリンクを開いた際に、まったく同じフィルタ処理済みの結果が表示されます。これは、useState を使用してフィルタ条件を管理している場合には不可能です。


5. ルーティング設定のための組織的戦略

プロジェクトのルート数が数十個に増えると、すべてのルートを単一のコンポーネントに記述していると、コードの保守が難しくなります。トムは、ルート設定を別のモジュールに抽出する必要があります。

(1) ルーティング設定ファイル

JSX
// src/routes/index.js
import { lazy } from 'react'

// Centrally manage all routing definitions
const routes = [
  {
    path: '/',
    component: lazy(() => import('../pages/Home')),
    exact: true
  },
  {
    path: '/login',
    component: lazy(() => import('../pages/Login')),
  },
  {
    path: '/dashboard',
    component: lazy(() => import('../pages/Dashboard')),
    protected: true  // You must log in
  },
  {
    path: '/admin',
    component: lazy(() => import('../pages/AdminPanel')),
    protected: true,
    adminOnly: true   // Required admin Permissions
  },
  {
    path: '/products',
    component: lazy(() => import('../pages/ProductList')),
  },
  {
    path: '/products/:id',
    component: lazy(() => import('../pages/ProductDetail')),
  },
  {
    path: '*',
    component: lazy(() => import('../pages/NotFound')),
  }
]

export default routes

(1) ▶ サンプル:ルート・レンダラー

JSX
// src/routes/AppRouter.jsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { Suspense } from 'react'
import routes from './index'
import ProtectedRoute from '../components/ProtectedRoute'
import AdminRoute from '../components/AdminRoute'

function renderRoutes(routeList) {
  return routeList.map(route => {
    const Component = route.component

    let element = <Component />

    // On-Demand Package Guard
    if (route.protected) {
      element = <ProtectedRoute>{element}</ProtectedRoute>
    }
    if (route.adminOnly) {
      element = <AdminRoute>{element}</AdminRoute>
    }

    return (
      <Route key={route.path} path={route.path} element={element} />
    )
  })
}

function AppRouter() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading......</div>}>
        <Routes>
          {renderRoutes(routes)}
        </Routes>
      </Suspense>
    </BrowserRouter>
  )
}

export default AppRouter
▶ 試してみよう

このアプローチの利点は、ルーティング設定が一箇所に集約され、ガードロジックが自動的にカプセル化され、新しいページを追加する際にはroutes/index.jsにオブジェクトを追加するだけで済む点です。

(2) useLocation:ルートの変更を監視する

ナビゲーションに加え、Tomは特定のシナリオにおいてルートの変更を監視する必要もあります。例えば、ページのパスが変更された際に追跡データを送信したり、ポップアップウィンドウを閉じたり、フォームの状態をリセットしたりする場合などです。useLocation を使用すると、現在の URL 情報を取得でき、useEffect と組み合わせて使用することで、パスの変更を監視することができます。

JSX
import { useLocation } from 'react-router-dom'
import { useEffect } from 'react'

function PageTracker() {
  const location = useLocation()

  useEffect(() => {
    // Triggered whenever the path changes
    console.log('Page Views:', location.pathname + location.search)

    // Tracking Event Reporting
    analytics.pageView({
      path: location.pathname,
      search: location.search,
      timestamp: Date.now()
    })
  }, [location])  // Dependency location Object
  // Note:location.pathname or  location.search Any change will trigger

  return null  // This component does not render anything UI
}

// in  App Used in
function App() {
  return (
    <BrowserRouter>
      <PageTracker />  {/* Place Routes External,Always Listen */}
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/dashboard" element={<Dashboard />} />
      </Routes>
    </BrowserRouter>
  )
}

useLocation が返す主なプロパティ: pathname(パス、例:/products/42)、search(クエリ文字列、例:?category=electronics)、 hash(URLハッシュ、例:#section-2)、stateLinkまたはnavigateを介して渡される状態データ)。


❓ よくある質問

Q useNavigateLink のどちらを選べばよいですか?
A Link は、ユーザーのクリックによってトリガーされるナビゲーション(ナビゲーションバー、ブレッドクラム、記事一覧内のリンクなど)に使用されます。useNavigateは、コードロジックによってトリガーされるナビゲーション(ログイン後やフォーム送信後のリダイレクト、スケジュールされたリダイレクト、または「戻る」「進む」ナビゲーションなど)に使用されます。基本的なルール:ユーザー操作 → Link;コードロジック → useNavigate
Q ルートガードは複数階層にネストできますか? 注意すべき点はありますか?
A はい。一般的なパターンとして、外側の ProtectedRoute ガードで「ユーザーがログインしているか」を確認し、内側の AdminRoute ガードで「ユーザーが管理者権限を持っているか」を確認します。ガードをネストさせる際は、各ガードが単一の責任のみを担うようにしてください。1つのガード内でログイン状態とロール権限の両方をチェックすることは避けてください。ガードが多すぎる場合は、設定配列とループを組み合わせて、複数層のガードを自動的にラップすることを検討してください。
Q React.lazy による遅延読み込みにはどのような制限がありますか?
A 3つの制限があります:(1) デフォルトエクスポート (export default) を使用してエクスポートされたコンポーネントでのみ使用可能です; (2) Suspenseコンポーネント内で使用する必要があります;(3) サーバーサイドレンダリングには対応していません(SSRの場合は、代わりに@loadable/componentを使用してください)。また、遅延読み込み中のコンポーネントの読み込み中にネットワーク速度が遅い場合、ユーザーにはフォールバックコンテンツが表示されます。フォールバックコンテンツは、サイズが小さく読み込みが速いように設計することをお勧めします。
Q フィルタ条件の管理において、useSearchParamsuseStateを使用する場合の違いは何ですか?
A useSearchParamsは状態をURLに同期するため、ユーザーはリンクの共有、ページのブックマーク登録、および前後のページへの移動が可能です。useStateで管理される状態はメモリ内にのみ存在し、ページが更新されると失われます。ただし、useSearchParams ではパフォーマンス上のオーバーヘッドが発生します(URL の変更によりコンポーネントの再レンダリングがトリガーされるため)。頻繁に更新が行われるシナリオ(スライダーのドラッグなど)では、まず useState を使用し、状態が確定した後に URL へ同期することをお勧めします。
Q ルートガードとミドルウェアの違いは何ですか?
A ルートガードはReactコンポーネントレベルでのインターセプターです。対象のコンポーネントをラップし、条件を確認した上で、そのコンポーネントをレンダリングするか、リダイレクトするかを決定します。一方、ミドルウェアはNext.jsのサーバーサイドレベルでのインターセプターです。リクエストがサーバーに到達した後、ページがレンダリングされる前に、認証、リダイレクト、A/Bテストなどのロジックを実行します。React Routerのガードはクライアント側で実行されるのに対し、Next.jsのミドルウェアはEdge Runtimeのサーバー側で実行されます。

📖 まとめ


📝 練習問題

  1. ログインページを実装する:ユーザーがユーザー名を入力した後、useNavigate を使用してダッシュボードに移動し、移動中は replace: true を使用して、ユーザーがログインページに戻れないようにする。
  2. ProtectedRoute を使用してダッシュボードへのアクセス経路を保護し、ユーザーがログインしていない場合はログインページにリダイレクトし、ログインに成功した後はユーザーが当初アクセスしようとしていたページに自動的にリダイレクトするようにします。
  3. useSearchParams を使用して商品一覧ページを実装してください。カテゴリによるフィルタリング、価格による並べ替え、ページネーションに対応している必要があります。フィルタ条件は URL に反映され、ページを更新した後もフィルタ条件が保持されるようにしてください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%