React Router の始め方
トムは、Eコマースの管理パネルを開発しています。当初、彼は「ページ」を切り替えるために
useStateを使用していました。具体的には、トップページ、商品一覧、注文詳細のそれぞれに1つずつ状態変数を割り当てていました。ページ数が10に増えると、状態管理のロジックは複雑に絡み合い、URLバーが/のまま固定されてしまい、特定のページへのリンクを直接共有することができなくなってしまいました。彼は、プロフェッショナルなルーティングソリューションが欠けていたことに気づいたのです。
1. 学習内容
- BrowserRouterとHashRouterのどちらを選ぶか判断するための基準
- 「Routes」と「Route」のパス照合メカニズム
- Link および NavLink による宣言型ナビゲーション
- useParams: 動的なURLパラメータを読み取ります
- ネストされたルートとコンセント配置モード
2. 概念図
flowchart LR
A[BrowserRouter<br/>Routing Container] --> B[Routes<br/>Routing Table]
B --> C["Route path='/'<br/>→ Home"]
B --> D["Route path='/products'<br/>→ ProductList"]
B --> E["Route path='/products/:id'<br/>→ ProductDetail"]
B --> F["Route path='*'<br/>→ NotFound"]
C --> G[Rendering Component]
D --> G
E --> G
F --> G
style A fill:#e1f5fe,stroke:#0288d1
style B fill:#fff3e0,stroke:#f57c00
style G fill:#e8f5e9,stroke:#388e3c
ユーザーが別のURLにアクセスする → BrowserRouterがそれを捕捉する → 最も適切なルートにマッチする → 対応するコンポーネントがレンダリングされる。
3. 実際の事例
トムの管理パネルには、「ダッシュボード」、「製品管理」、「システム設定」という3つのメインページが必要です。さらに、「製品管理」セクションには、「製品一覧」と「製品詳細」という2つのサブページが含まれています。彼は、ユーザーがブラウザの「進む」ボタンや「戻る」ボタンを使って移動できるように、各ページに固有のURLを設定したいと考えています。
(1) ルーティングモードの選択
React Router には 2 つのルーティングモードがあり、これらは URL の扱い方が根本的に異なります:
| パターン | URLの例 | 原理 | ユースケース |
|---|---|---|---|
BrowserRouter |
example.com/users |
History API を使用した URL の操作 | サーバー側で URL の書き換えを設定できるプロジェクト |
HashRouter |
example.com/#/users |
URLのハッシュ変更によって引き起こされるサーバーリクエストの防止 | 静的ホスティング(GitHub Pages、CDN) |
推奨事項: サーバーでホストできるプロジェクトについては、常に BrowserRouter を使用してください。これにより、URL がすっきりし、SEO 対策にも適しています。静的ホスティングの場合は、HashRouter を使用してください。
(1) ▶ サンプル:基本的なルーティング設定
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'
function Home() {
return <h2>Dashboard Home Page</h2>
}
function ProductList() {
return <h2>Product List</h2>
}
function Settings() {
return <h2>System Settings</h2>
}
function NotFound() {
return <h2>404 — Page Not Found</h2>
}
function App() {
return (
<BrowserRouter>
<nav style={{ display: 'flex', gap: '1rem', padding: '1rem', background: '#f0f0f0' }}>
<Link to="/">Home</Link>
<Link to="/products">Product Management</Link>
<Link to="/settings">System Settings</Link>
</nav>
<main style={{ padding: '1rem' }}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<ProductList />} />
<Route path="/settings" element={<Settings />} />
<Route path="*" element={<NotFound />} />
</Routes>
</main>
</BrowserRouter>
)
}
export default App
仕組み: ナビゲーションリンクをクリックすると、URLが変更され、ページの内容が更新されますが、ブラウザはページ全体を再読み込みしません。これがSPAのルーティングにおける中核となる動作です。
path="*" について: ワイルドカード * は、それ以前のルートで定義されていないすべてのパスに一致します。通常、ルートリストの最後に配置して 404 ページを生成します。React Router v6 では、* はパスの末尾の文字としてのみ使用できます。
(2) NavLinkの有効化状況
| ナビゲーションコンポーネント | 目的 | 起動方法 | ユースケース |
|---|---|---|---|
<Link to="/path"> |
宣言型ナビゲーション | なし | 一般的なナビゲーションリンク |
<NavLink to="/path"> |
アクティブ状態のナビゲーション | isActive コールバック |
サイドバー/トップナビゲーションのハイライト |
navigate('/path') |
プログラムによるリダイレクト | — | ログイン後のリダイレクト、フォーム送信後のリダイレクト |
<Navigate to="/path" /> |
宣言型リダイレクト | — | 条件付きリダイレクトコンポーネント |
トムは、ユーザーが「現在どこにいるか」を明確に把握できるよう、ナビゲーションバーで現在のページを強調表示したいと考えています。<NavLink> コンポーネントには isActive パラメータが用意されており、これを使用すると現在のルートに基づいてスタイルを動的に設定できます。<Link> と比較して、2つの追加プロパティがあります。style と className はどちらも、isActive および isPending プロパティを使用してコールバック関数を受け入れることをサポートしています。
(2) ▶ サンプル:アクティブスタイルが適用されたナビゲーションバー
import { NavLink } from 'react-router-dom'
function NavBar() {
const linkStyle = {
padding: '8px 16px',
textDecoration: 'none',
borderRadius: '6px',
transition: 'all 0.2s'
}
const activeStyle = {
...linkStyle,
backgroundColor: '#1976d2',
color: '#fff',
fontWeight: 'bold'
}
const inactiveStyle = {
...linkStyle,
color: '#333'
}
return (
<nav style={{ display: 'flex', gap: '12px', padding: '12px', background: '#fafafa' }}>
<NavLink
to="/"
style={({ isActive }) => (isActive ? activeStyle : inactiveStyle)}
end // Exact Match,Avoid "/" Match all that start with "/" Starting path
>
Home
</NavLink>
<NavLink
to="/products"
style={({ isActive }) => (isActive ? activeStyle : inactiveStyle)}
>
Product Management
</NavLink>
<NavLink
to="/settings"
style={({ isActive }) => (isActive ? activeStyle : inactiveStyle)}
>
System Settings
</NavLink>
</nav>
)
}
要点: end プロパティを設定すると、/ パスは完全に一致する場合にのみ有効になります。それ以外の場合は、すべてのパスでホームページのアクティベーションスタイルが適用されます。また、NavLink の className はコールバック関数にも対応しているため、CSS クラス名を使用するプロジェクトに適しています。
(3) ネストされたルートとレイアウトの共有
トムは、プロダクト管理ページに「プロダクト一覧」と「プロダクト追加」という2つのサブページがあり、それらが同じサイドバーレイアウトを共有していることに気づきました。サブページごとにレイアウトコードを繰り返し記述するのは、冗長であるだけでなく、メンテナンスも困難になります。React Routerのネストされたルーティングと<Outlet>を組み合わせることで、この問題を完璧に解決できます。
ネストされたルーティングの核心となる概念は、親ルートがレイアウトの枠組みを定義し、子ルートがアウトレットを介してコンテンツを注入するという点です。親コンポーネントは、子ルートが具体的に何をレンダリングするかについては関与せず、レイアウトの枠組みのみを担当します。
(3) ▶ サンプル:ネストされたルートとアウトレット
import { BrowserRouter, Routes, Route, Link, Outlet, useParams } from 'react-router-dom'
// Parent Layout Component — Shared Sidebar + Outlet
function ProductsLayout() {
return (
<div style={{ display: 'flex' }}>
<aside style={{ width: '200px', padding: '16px', background: '#f5f5f5' }}>
<h3>Product Management</h3>
<nav style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<Link to="list">Product List</Link>
<Link to="add">Add Item</Link>
</nav>
</aside>
<main style={{ flex: 1, padding: '16px' }}>
{/* The components of the child route are rendered here */}
<Outlet />
</main>
</div>
)
}
function ProductList() {
const products = [
{ id: 1, name: 'React Programming Books', price: 79 },
{ id: 2, name: 'TypeScript Guide', price: 59 },
{ id: 3, name: 'Node.js Real-World Experience', price: 69 }
]
return (
<div>
<h2>Product List</h2>
<ul>
{products.map(p => (
<li key={p.id}>
<Link to={`/products/detail/${p.id}`}>
{p.name} — ${p.price}
</Link>
</li>
))}
</ul>
</div>
)
}
function AddProduct() {
return (
<div>
<h2>Add Item</h2>
<form onSubmit={e => { e.preventDefault(); alert('Submission Successful!') }}>
<div><label>Product Name:<input name="name" /></label></div>
<div><label>Price:<input name="price" type="number" /></label></div>
<button type="submit">Submit</button>
</form>
</div>
)
}
function ProductDetail() {
const { id } = useParams()
return <h2>Product Details(ID:{id})</h2>
}
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<h2>Home</h2>} />
{/* Nested Routes:Parent Route with Layout,Subnet routing is enabled Outlet Rendering */}
<Route path="/products" element={<ProductsLayout />}>
<Route index element={<ProductList />} /> {/* /products Default Display */}
<Route path="list" element={<ProductList />} /> {/* /products/list */}
<Route path="add" element={<AddProduct />} /> {/* /products/add */}
<Route path="detail/:id" element={<ProductDetail />} /> {/* /products/detail/1 */}
</Route>
<Route path="*" element={<h2>404 Not found</h2>} />
</Routes>
</BrowserRouter>
)
}
フローロジック: /products/list にアクセス → ProductsLayout (サイドバーをレンダリング) → Outlet (その位置に ProductList コンポーネントを表示)。URL 構造はコンポーネント構造と一対一で対応しているため、明確でメンテナンスも容易です。
インデックスルートについて: index ルートは、親パスのデフォルトのサブルートです。/products にアクセスした際、一致するサブパスがない場合(list にも add にも一致しない場合)、インデックスルートのコンテンツがアウトレットに表示されます。これにより、親パスに空白領域が表示されないようになります。
4. 動的ルーティングとパスパラメータ
| パスパターン | URLの例 | useParamsの戻り値 | 説明 |
|---|---|---|---|
/users/:id |
/users/42 |
{ id: '42' } |
単一動的パラメータ |
/users/:userId/posts/:postId |
/users/42/posts/99 |
{ userId: '42', postId: '99' } |
多段動的パラメータ |
/files/* |
/files/a/b/c |
{ '*': 'a/b/c' } |
残りのパスに一致するワイルドカード |
/categories/:catId/:tab? |
2つのルートを定義する必要があります | { catId, tab } |
オプションのパラメータ(ネイティブには未対応) |
動的ルーティングは、ルーティングシステムの最も重要な機能の一つです。トムの商品詳細ページでは、商品IDに応じて異なるコンテンツを表示する必要がありますが、すべての商品IDに対して個別にルートを作成することは不可能です。そこで、動的なパスを定義するために:param構文が活用されます。
(1) useParams の使用
:id は動的パラメータのプレースホルダーです。実際の値は、useParams フックを介して URL から抽出されます。
function UserDetail() {
const { userId, postId } = useParams()
return <p>User {userId} Article {postId}</p>
}
// URL: /users/42/posts/99 → userId=42, postId=99
(1) ▶ サンプル:商品詳細ページ
import { useParams, Link, useNavigate } from 'react-router-dom'
// Simulated Product Data
const products = [
{ id: '1', name: 'React Programming Books', price: 79, description: 'Mastering It from Scratch React 18 Development' },
{ id: '2', name: 'TypeScript Guide', price: 59, description: 'Systematic Study TypeScript Type System' },
{ id: '3', name: 'Node.js Real-World Experience', price: 69, description: 'Backend Development: From Beginner to Advanced' }
]
function ProductDetail() {
const { id } = useParams()
const navigate = useNavigate()
const product = products.find(p => p.id === id)
if (!product) {
return (
<div>
<h2>The product does not exist.</h2>
<button onClick={() => navigate('/products')}>Back to Product List</button>
</div>
)
}
return (
<div>
<h2>{product.name}</h2>
<p className="price">Price:${product.price}</p>
<p className="desc">{product.description}</p>
<Link to="/products">← Back to List</Link>
</div>
)
}
注: 動的パラメータを使用する場合、「パラメータの値に対応するデータがない」という状況に備える必要があります。上記のコードにある if (!product) 分岐は、存在しない ID にアクセスしようとした際にページが空白画面を表示しないよう、空のデータを処理するものです。
(2) マルチセグメントの動的パラメータ
パスには複数の動的パラメータを含めることができ、これはリソースがネストされているシナリオでよく見られます:
// Route Definitions
<Route path="/categories/:catId/products/:prodId" element={<ProductView />} />
// Extracted from the component
function ProductView() {
const { catId, prodId } = useParams()
// URL: /categories/electronics/products/42
// catId = "electronics", prodId = "42"
return <h2>Categories {catId} Items under {prodId}</h2>
}
(3) オプションのパラメータとワイルドカード
React Router v6 はオプションのパラメータを直接サポートしていませんが、2つの方法で同様の効果を実現できます:
// Method 1:Define two Route(Recommendations)
<Route path="/categories/:catId" element={<CategoryPage />} />
<Route path="/categories/:catId/:tab" element={<CategoryPage />} />
// Method 2:Make the determination within the component itself
function CategoryPage() {
const { catId, tab } = useParams()
const activeTab = tab || 'overview' // Default value
return <h2>Categories {catId} - {activeTab}</h2>
}
オプション1の利点は、URLの意味が明確であることです。一方、オプション2はより簡潔ですが、URLの読みやすさが損なわれます。
(4) パスマッチングの優先順位の詳細な説明
React Router v6におけるパスマッチングは、従来のフレームワークで採用されていた「先着順」のアプローチではなく、スコアリングアルゴリズムに基づいています。これらのルールを理解しておくことで、ルートが期待通りに動作しない場合のトラブルシューティングに役立ちます。
// Suppose we have the following routing configuration
<Routes>
<Route path="/products/new" element={<NewProduct />} /> {/* Static Path */}
<Route path="/products/:id" element={<ProductDetail />} /> {/* Dynamic Path */}
<Route path="/products/:id/edit" element={<EditProduct />} /> {/* Hybrid Path */}
</Routes>
マッチの優先順位(高い順):
- 静的パスセグメント (
new) は、動的パラメータセグメント (:id) よりも優先される - 静的セグメントが多いパスは、静的セグメントが少ないパスよりも優先される
- セグメントの数が多いパスは、セグメントの数が少ないパスよりも優先される。
したがって、/products/new へのリクエストは <NewProduct /> と一致し、/products/42 へのリクエストは <ProductDetail /> と一致し、/products/42/edit へのリクエストは <EditProduct /> と一致します。開発者は順序を気にする必要はありません。システムが自動的に「最適な一致」を選択します。
❓ よくある質問
/products/detail/1 をリクエストするものの、そのファイルがサーバー上に存在しないためです。解決策:Nginxでtry_files $uri $uri/ /index.htmlを設定し、すべてのルートリクエストをindex.htmlにリダイレクトするようにし、フロントエンドでのマッチングはReact Routerに処理させます。サーバーの設定ができない場合は、HashRouterに切り替えてください(#以降の部分はサーバーに送信されません)。<Link>とネイティブの<a>タグの違いは何ですか?<a>をクリックすると、ブラウザでページ全体が再読み込みされ、SPAのメモリ内の状態がすべて失われてしまいます。<Link>は、デフォルトのナビゲーションを阻止し、History APIを介してURLを更新し、React Routerに新しいコンポーネントをレンダリングするよう通知します。これにより、ページのリフレッシュや状態の喪失を回避できます。SPAでは、<a>の代わりに常にLinkまたはNavLinkを使用してください。/users/new は /users/:id よりも特異性が高いため、優先されます。*は、一致しないすべてのパスにマッチするワイルドカードとして機能し、通常は404ページとして機能させるために最後に配置されます。パスはデフォルトでプレフィックス一致を使用しますが、end属性を追加すると、完全一致に変更されます。<Route index element={...} /> は、親ルートのデフォルトのサブルートを定義します。親ルート自体(例:/products)にアクセスした際、その親ルートがアウトレットを使用している場合、「index」ルートのコンテンツがアウトレットに表示されます。これは「親パス下のデフォルトページ」として機能し、親パスにアクセスした際に空白の領域が表示されるのを防ぎます。<Routes> + <Route> が <Switch> に置き換えられ、Routeコンポーネントが自動的に最も具体的なパスにマッチするようになりました;② ネストされたルートでは、子ルートを手動でレンダリングする代わりに <Outlet> を使用するようになりました; ③ useNavigate()がuseHistory()に、navigate('/path')がhistory.push('/path')に、navigate(-1)がhistory.goBack()に置き換わりました。v6のAPIはより簡潔ですが、移行には大幅なコードの変更が必要となります。(5) 相対パスと絶対パスの違い
ネストされたルーティングでは、<Link to="..."> のパスは現在のルートに対する相対パスであるのに対し、<Link to="/..."> は絶対パスです。この違いを理解することは、ナビゲーションエラーを回避するために極めて重要です。
// Currently in /products under (ProductsLayout Within the component)
<Link to="list"> {/* → /products/list(Relative Path,After appending it to the current route) */}
<Link to="/list"> {/* → /list(Absolute Path,Replace directly) */}
<Link to="../settings"> {/* → /settings(Parent-level relative path) */}
ネストされたルーティングにおいて、子ルートが ProductsLayout 内に位置する場合、その内部にあるすべてのリンクは相対パス(先頭の / を省略したもの)を使用する必要があります。これにより、親ルートのパスが変更された際、子ルートのリンクが自動的に適応するようになります。
5. 404ページとルーティングの設計パターン
(1) ワイルドカードルーティング
path="*" は定義されていないすべてのルートに一致し、404ページを実装するための標準的な方法です。ただし、React Router v6 では、* はルートの末尾にのみ配置でき、path="/users/*/edit" のように使用することはできません。
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<ProductList />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/about" element={<About />} />
{/* Wildcard routes must be placed last */}
<Route path="*" element={<NotFound />} />
</Routes>
)
}
function NotFound() {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<h1>404</h1>
<p>Sorry, the page you are looking for does not exist.。</p>
<Link to="/">Back to Home</Link>
</div>
)
}
(2) useNavigate の基本的な使い方
useNavigateについては上級コースで詳しく取り上げる予定ですが、トムは初心者段階でも、ログインページのカウントダウン終了後のリダイレクトや、フォーム送信後のリダイレクトなど、特定の場面でこれを使用する必要があります。
import { useNavigate } from 'react-router-dom'
function OrderSuccess() {
const navigate = useNavigate()
const [countdown, setCountdown] = useState(5)
useEffect(() => {
const timer = setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
clearInterval(timer)
navigate('/orders') // Automatically redirect when the countdown ends
return 0
}
return prev - 1
})
}, 1000)
return () => clearInterval(timer)
}, [navigate])
return (
<div>
<h2>Order Placed Successfully!</h2>
<p>{countdown} You will be automatically redirected to the order page in seconds.</p>
<button onClick={() => navigate('/orders')}>Check it out now</button>
</div>
)
}
(1) ▶ サンプル:すべての機能を統合した管理パネル
このレッスンで学んだすべての概念――BrowserRouter、Routes、ネストされたルート、NavLink、useParams、404ページ――を統合して、完全な管理ダッシュボードの骨組みを構築しましょう。
import { BrowserRouter, Routes, Route, NavLink, Outlet, useParams } from 'react-router-dom'
import './App.css'
// Layout Components
function AdminLayout() {
return (
<div className="admin-container">
<header className="admin-header">
<h1>Tom E-commerce Management Backend</h1>
</header>
<div className="admin-body">
<nav className="admin-sidebar">
<NavLink to="/" end>Dashboard</NavLink>
<NavLink to="/products">Product Management</NavLink>
<NavLink to="/orders">Order Management</NavLink>
<NavLink to="/settings">System Settings</NavLink>
</nav>
<main className="admin-content">
<Outlet />
</main>
</div>
</div>
)
}
// Page Components
function Dashboard() {
return <h2>Welcome back,Tom!Number of Orders Today:42</h2>
}
function ProductsLayout() {
return (
<div>
<h2>Product Management</h2>
<nav>
<NavLink to="list">Product List</NavLink>
<NavLink to="add">Add Item</NavLink>
</nav>
<Outlet />
</div>
)
}
function ProductList() {
return <p>The product list is displayed here...</p>
}
function AddProduct() {
return <p>The form for adding products is displayed here....</p>
}
function Orders() {
return <h2>Order Management</h2>
}
function Settings() {
return <h2>System Settings</h2>
}
function NotFound() {
return <h2>404 — Page Not Found</h2>
}
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<AdminLayout />}>
<Route index element={<Dashboard />} />
<Route path="products" element={<ProductsLayout />}>
<Route index element={<ProductList />} />
<Route path="list" element={<ProductList />} />
<Route path="add" element={<AddProduct />} />
</Route>
<Route path="orders" element={<Orders />} />
<Route path="settings" element={<Settings />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
)
}
アーキテクチャの特徴: アプリケーション全体は、1つの<BrowserRouter>と1つのトップレベル<Routes>のみで構成されています。AdminLayoutは、アウトレットを介してすべてのページコンポーネントを埋め込むグローバルレイアウトとして機能します。この「シングルルート+ネストされたレイアウト」というパターンは、中規模のReactアプリケーションにおける標準的なアーキテクチャです。
📖 まとめ
- BrowserRouter は History API に基づいており、HashRouter は URL のハッシュに基づいています。前者を使用することをお勧めします(サーバー側のサポートが必要です)。
- Routes コンポーネントはマッチングを処理し、Route コンポーネントはパスとコンポーネント間のマッピングを定義します
- リンク:宣言型ナビゲーション用。NavLink はアクティブ状態の検出機能(isActive コールバック)を提供します。
- useParams は URL から動的パラメータを抽出しており、複数セグメントのパラメータ (
:id,:catId/:prodId) にも対応しています。 - ネストされたルートでは、アウトレットを使用してレイアウトを再利用し、「index」ルートが親パスに対してデフォルトのコンテンツを提供します
- 本番環境でBrowserRouterを展開する際は、サーバーのURL書き換えルールを設定する必要があります。
- ネストされたルーティングでは、相対パス(先頭に
/がないもの)と絶対パス(先頭に/があるもの)の違いに注意してください。
📝 練習問題
- 4ページからなる管理ダッシュボードを作成してください:ホーム、ユーザー一覧、ユーザー詳細(
useParamsを使用してユーザー ID を取得)、および「About」ページ。ナビゲーションにはLinkを使用し、NavLinkに有効なスタイルが適用されていることを確認してください。 - ユーザー一覧ページにネストされたルートを追加します。
/usersはユーザー一覧を表示し、/users/:idはユーザーの詳細を表示します。両者は、ヘッダーを含むレイアウトコンポーネントを(アウトレットを使用して)共有しています。 - ルーティングテーブルの最後に404ページを追加し、ユーザーが存在しないパスにアクセスした際に、わかりやすいメッセージが表示されるようにする。



