React Router:応用編
トムは基本的なルーティングの実装に成功しましたが、新たな要件が次々と浮上しています。具体的には、ユーザーがログイン後に自動的にダッシュボードにリダイレクトされる必要があること、特定のページにアクセスするにはログインが必要であること、アプリの規模拡大に伴いホームページの読み込みに時間がかかるようになったこと、そして商品一覧ページでURL内のフィルタパラメータに対応する必要があることなどです……。こうした実世界のシナリオに対処するためには、React Routerの高度な機能を学ぶ必要があります。
1. 学習内容
- useNavigate: プログラムによるナビゲーション(ログイン後のリダイレクト、戻る・進む)
- Route Guard コンポーネントは、認証と認可を実装しています
- オンデマンド読み込みのための React.lazy と Suspense
- useSearchParams: URLのクエリパラメータを管理する
- ルーティング設定の整理および抽出のための戦略
2. 概念図
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) ▶ サンプル:ログイン後のプログラムによるナビゲーション
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) ▶ サンプル:多層ルーティングガード
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 を使えば、コンポーネントのコードを個別のチャンクに分割し、必要なときにのみ読み込むことができます。
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) ▶ サンプル:ルートレベルでの遅延読み込み
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 と同様に、値とセッターを返します。
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) ▶ サンプル:製品フィルタリング機能の完成形
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) ルーティング設定ファイル
// 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) ▶ サンプル:ルート・レンダラー
// 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 と組み合わせて使用することで、パスの変更を監視することができます。
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)、state(Linkまたはnavigateを介して渡される状態データ)。
❓ よくある質問
useNavigate と Link のどちらを選べばよいですか?Link は、ユーザーのクリックによってトリガーされるナビゲーション(ナビゲーションバー、ブレッドクラム、記事一覧内のリンクなど)に使用されます。useNavigateは、コードロジックによってトリガーされるナビゲーション(ログイン後やフォーム送信後のリダイレクト、スケジュールされたリダイレクト、または「戻る」「進む」ナビゲーションなど)に使用されます。基本的なルール:ユーザー操作 → Link;コードロジック → useNavigate。ProtectedRoute ガードで「ユーザーがログインしているか」を確認し、内側の AdminRoute ガードで「ユーザーが管理者権限を持っているか」を確認します。ガードをネストさせる際は、各ガードが単一の責任のみを担うようにしてください。1つのガード内でログイン状態とロール権限の両方をチェックすることは避けてください。ガードが多すぎる場合は、設定配列とループを組み合わせて、複数層のガードを自動的にラップすることを検討してください。export default) を使用してエクスポートされたコンポーネントでのみ使用可能です; (2) Suspenseコンポーネント内で使用する必要があります;(3) サーバーサイドレンダリングには対応していません(SSRの場合は、代わりに@loadable/componentを使用してください)。また、遅延読み込み中のコンポーネントの読み込み中にネットワーク速度が遅い場合、ユーザーにはフォールバックコンテンツが表示されます。フォールバックコンテンツは、サイズが小さく読み込みが速いように設計することをお勧めします。useSearchParamsとuseStateを使用する場合の違いは何ですか?useSearchParamsは状態をURLに同期するため、ユーザーはリンクの共有、ページのブックマーク登録、および前後のページへの移動が可能です。useStateで管理される状態はメモリ内にのみ存在し、ページが更新されると失われます。ただし、useSearchParams ではパフォーマンス上のオーバーヘッドが発生します(URL の変更によりコンポーネントの再レンダリングがトリガーされるため)。頻繁に更新が行われるシナリオ(スライダーのドラッグなど)では、まず useState を使用し、状態が確定した後に URL へ同期することをお勧めします。📖 まとめ
- useNavigate は、コードによってトリガーされるナビゲーション(ログイン後のリダイレクト、前へ・次への移動)を処理し、
replace: trueにより履歴スタックの破損を防ぎます - ルーターガードはラッパーコンポーネントを使用して実装されています。外側のレイヤーが認証を確認し、内側のレイヤーが権限を確認することで、単一責任の原則と再利用性が確保されています。
- React.lazy と Suspense を組み合わせることで、ルート単位でのコードバンドリングが可能になり、ファーストスクリーンの読み込み速度が大幅に向上します
- useSearchParams は、フィルタ設定とページネーション設定を URL に反映させ、リンクの共有やブラウザの「進む」「戻る」操作に対応しています
- ルート設定の一元管理と自動レンダリングガード機能により、大規模プロジェクトの維持管理に最適
📝 練習問題
- ログインページを実装する:ユーザーがユーザー名を入力した後、
useNavigateを使用してダッシュボードに移動し、移動中はreplace: trueを使用して、ユーザーがログインページに戻れないようにする。 ProtectedRouteを使用してダッシュボードへのアクセス経路を保護し、ユーザーがログインしていない場合はログインページにリダイレクトし、ログインに成功した後はユーザーが当初アクセスしようとしていたページに自動的にリダイレクトするようにします。useSearchParamsを使用して商品一覧ページを実装してください。カテゴリによるフィルタリング、価格による並べ替え、ページネーションに対応している必要があります。フィルタ条件は URL に反映され、ページを更新した後もフィルタ条件が保持されるようにしてください。



