Reactルーティング入門|ページ遷移の仕組みを基礎から解説
React Routerを使ったページ遷移の基礎から応用まで詳しく解説。ルーティングの概念、実装方法、動的ルート、ネストしたルートまで実践的に学べます。
Reactルーティング入門|ページ遷移の仕組みを基礎から解説
みなさん、Reactでアプリを作っていて「ページ遷移ってどうやるの?」と困ったことはありませんか?
「React Routerって聞くけど難しそう...」 「URLが変わるのにページが再読み込みされないのはなぜ?」 そんな疑問を持ったことがある方は多いのではないでしょうか?
確かに、Reactのルーティングは従来のWebサイトとは仕組みが異なります。 最初は戸惑うものですが、一度理解してしまえばとても便利なんです!
この記事では、Reactのルーティングを基礎から詳しく解説します。 React Routerの使い方から動的ルート、ネストしたルートまで、実践的なコード例とともにお伝えしますね。 読み終わる頃には、自信を持ってReactアプリにページ遷移機能を実装できるはずです!
Reactルーティングって何?基本を理解しよう
なぜルーティングが必要なの?
まず、通常のWebサイトとReactアプリの違いを理解しましょう。
従来のWebサイトでは、ページごとに別々のHTMLファイルが存在します。 しかし、ReactのSPA(Single Page Application)では、1つのHTMLファイルでアプリ全体を管理します。
従来のWebサイトの構造
/index.html → ホームページ
/about.html → アバウトページ
/contact.html → コンタクトページ
/products/1.html → 商品詳細ページ
各ページが独立したHTMLファイルになっていますね。
ReactのSPAの構造
/index.html → すべてのページを管理
├─ / (ホーム)
├─ /about (アバウト)
├─ /contact (コンタクト)
└─ /products/1 (商品詳細)
1つのHTMLファイルで、すべてのページを表現します。
SPAでのルーティングの仕組み
SPAでは、JavaScriptでコンテンツを切り替えてページ遷移を実現します。
// 従来のWebサイト:ページ遷移時に完全に再読み込み
// <a href="/about.html">アバウト</a>
// → サーバーから新しいHTMLを取得
// React SPA:JavaScript でコンテンツを切り替え
function App() {
const [currentPage, setCurrentPage] = useState('home');
const renderPage = () => {
switch (currentPage) {
case 'home': return <HomePage />;
case 'about': return <AboutPage />;
case 'contact': return <ContactPage />;
default: return <HomePage />;
}
};
return (
<div>
<nav>
<button onClick={() => setCurrentPage('home')}>ホーム</button>
<button onClick={() => setCurrentPage('about')}>アバウト</button>
<button onClick={() => setCurrentPage('contact')}>コンタクト</button>
</nav>
{renderPage()}
</div>
);
}
上記のコードでは、stateを使ってページを切り替えています。
ボタンを押すと、currentPage
の値が変わり、対応するコンポーネントが表示されます。
でも、この方法には問題があります。
主な問題点
- URLが変わらない(ブックマークできない)
- ブラウザの戻るボタンが使えない
- SEOに不利
これらの問題を解決するのが、React Routerなんです!
React Routerが解決してくれること
React Routerは、これらの問題をすべて解決します。
React Routerの主な機能
- URLとコンポーネントの対応付け
- ブラウザ履歴の管理
- プログラマティックナビゲーション
- 動的ルートパラメータ
// React Router を使用した例
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">ホーム</Link>
<Link to="/about">アバウト</Link>
<Link to="/contact">コンタクト</Link>
</nav>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<ContactPage />} />
</Routes>
</BrowserRouter>
);
}
このコードのポイントを説明しますね。
BrowserRouter アプリ全体をBrowserRouterで囲むことで、ルーティング機能が使えるようになります。
Link
通常の<a>
タグの代わりに使います。
ページの再読み込みなしで、URLを変更してくれます。
Routes と Route URLのパスと表示するコンポーネントを対応付けます。
これにより、URLが適切に変更され、ブラウザの戻るボタンも正常に動作します!
React Routerの基本セットアップ
インストール方法
まず、React Routerをインストールしましょう。
# npm の場合
npm install react-router-dom
# yarn の場合
yarn add react-router-dom
パッケージ名がreact-router-dom
であることに注意してくださいね。
基本的な設定
インストールが完了したら、基本的な設定を行います。
// App.js
import React from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import HomePage from './components/HomePage';
import AboutPage from './components/AboutPage';
import ContactPage from './components/ContactPage';
function App() {
return (
<BrowserRouter>
<div className="app">
<header>
<Navigation />
</header>
<main>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<ContactPage />} />
</Routes>
</main>
</div>
</BrowserRouter>
);
}
export default App;
この基本構成では、以下のようになっています。
アプリ全体をBrowserRouterで囲む これにより、アプリ内でルーティング機能が使えるようになります。
Routesで複数のRouteを囲む 複数のルートを定義する際は、Routesで囲む必要があります。
各Routeでパスとコンポーネントを対応付け
path
でURL、element
で表示するコンポーネントを指定します。
ナビゲーションコンポーネントを作る
次に、ナビゲーション用のコンポーネントを作成しましょう。
// components/Navigation.js
import React from 'react';
import { Link, NavLink } from 'react-router-dom';
function Navigation() {
return (
<nav className="navigation">
<div className="nav-brand">
<Link to="/">マイサイト</Link>
</div>
<ul className="nav-links">
<li>
<NavLink
to="/"
className={({ isActive }) => isActive ? 'active' : ''}
>
ホーム
</NavLink>
</li>
<li>
<NavLink
to="/about"
className={({ isActive }) => isActive ? 'active' : ''}
>
アバウト
</NavLink>
</li>
<li>
<NavLink
to="/contact"
className={({ isActive }) => isActive ? 'active' : ''}
>
コンタクト
</NavLink>
</li>
</ul>
</nav>
);
}
export default Navigation;
ここでは、Link
とNavLink
という2つのコンポーネントを使っています。
Link 基本的なリンクコンポーネントです。 クリックすると、指定したパスに移動します。
NavLink
Linkの拡張版で、現在のページかどうかを判定できます。
isActive
を使って、アクティブなリンクにスタイルを適用できます。
基本的なページコンポーネント
最後に、各ページのコンポーネントを作成します。
// components/HomePage.js
import React from 'react';
function HomePage() {
return (
<div className="page">
<h1>ホームページ</h1>
<p>ようこそ、マイサイトへ!</p>
<div className="features">
<h2>サイトの特徴</h2>
<ul>
<li>React Router を使ったスムーズなページ遷移</li>
<li>レスポンシブデザイン</li>
<li>モダンなユーザーインターフェース</li>
</ul>
</div>
</div>
);
}
export default HomePage;
// components/AboutPage.js
import React from 'react';
import { Link } from 'react-router-dom';
function AboutPage() {
return (
<div className="page">
<h1>アバウト</h1>
<p>このサイトについて詳しく説明します。</p>
<section>
<h2>ミッション</h2>
<p>私たちは最高のユーザーエクスペリエンスを提供します。</p>
</section>
<section>
<h2>チーム</h2>
<p>経験豊富な開発者たちがサービスを支えています。</p>
</section>
<div className="cta">
<p>ご質問がありましたら、お気軽にお問い合わせください。</p>
<Link to="/contact" className="btn btn-primary">
お問い合わせ
</Link>
</div>
</div>
);
}
export default AboutPage;
これで基本的なルーティングの設定が完了です! ブラウザで確認すると、URLが変わりながらページが切り替わることが確認できますよ。
動的ルートとパラメータ
URLパラメータの基本
動的ルートを使うと、URLの一部を変数として扱えます。 例えば、ユーザーのプロフィールページや商品詳細ページなどで活用できます。
// 動的ルートの定義
function App() {
return (
<BrowserRouter>
<Routes>
{/* 基本的なルート */}
<Route path="/" element={<HomePage />} />
{/* 動的パラメータ */}
<Route path="/users/:id" element={<UserProfile />} />
<Route path="/products/:categoryId/:productId" element={<ProductDetail />} />
{/* オプショナルパラメータ */}
<Route path="/blog/:slug?" element={<BlogPost />} />
</Routes>
</BrowserRouter>
);
}
上記のコードでは、以下のような動的ルートを定義しています。
:id
- 単一パラメータ
/users/123
のようなURLで、id
に123
が入ります。
:categoryId/:productId
- 複数パラメータ
/products/electronics/456
のようなURLで、カテゴリIDと商品IDを両方取得できます。
:slug?
- オプショナルパラメータ
?
を付けると、そのパラメータは省略可能になります。
useParamsフックでパラメータを取得
定義した動的ルートのパラメータは、useParams
フックで取得できます。
// components/UserProfile.js
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
function UserProfile() {
const { id } = useParams(); // URLパラメータを取得
const navigate = useNavigate();
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchUser() {
try {
setLoading(true);
setError(null);
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error('ユーザーが見つかりません');
}
const userData = await response.json();
setUser(userData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
if (id) {
fetchUser();
}
}, [id]);
const handleGoBack = () => {
navigate(-1); // 前のページに戻る
};
const handleGoToEdit = () => {
navigate(`/users/${id}/edit`); // 編集ページへ
};
if (loading) return <div className="loading">読み込み中...</div>;
if (error) return <div className="error">エラー: {error}</div>;
if (!user) return <div className="not-found">ユーザーが見つかりません</div>;
return (
<div className="user-profile">
<button onClick={handleGoBack} className="btn btn-secondary">
← 戻る
</button>
<div className="profile-header">
<img src={user.avatar} alt={user.name} className="avatar" />
<h1>{user.name}</h1>
<p className="bio">{user.bio}</p>
</div>
<div className="profile-details">
<div className="detail-item">
<label>メール:</label>
<span>{user.email}</span>
</div>
<div className="detail-item">
<label>職業:</label>
<span>{user.occupation}</span>
</div>
<div className="detail-item">
<label>所在地:</label>
<span>{user.location}</span>
</div>
</div>
<div className="actions">
<button onClick={handleGoToEdit} className="btn btn-primary">
プロフィール編集
</button>
</div>
</div>
);
}
export default UserProfile;
このコンポーネントのポイントを説明しますね。
useParamsでIDを取得
const { id } = useParams();
URLからid
パラメータを取得します。
useNavigateでページ遷移
const navigate = useNavigate();
navigate(-1); // 前のページに戻る
navigate(`/users/${id}/edit`); // 特定のページに移動
プログラムからページ遷移を行うときに使います。
useEffectでデータ取得
useEffect(() => {
// APIからユーザーデータを取得
}, [id]);
URLパラメータが変わったときに、対応するデータを取得します。
複数パラメータの使用例
複数のパラメータを使った商品詳細ページの例も見てみましょう。
// components/ProductDetail.js
import React, { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
function ProductDetail() {
const { categoryId, productId } = useParams();
const [product, setProduct] = useState(null);
const [category, setCategory] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function fetchProductData() {
try {
setLoading(true);
// 商品とカテゴリ情報を並行取得
const [productResponse, categoryResponse] = await Promise.all([
fetch(`/api/products/${productId}`),
fetch(`/api/categories/${categoryId}`)
]);
const productData = await productResponse.json();
const categoryData = await categoryResponse.json();
setProduct(productData);
setCategory(categoryData);
} catch (error) {
console.error('データ取得エラー:', error);
} finally {
setLoading(false);
}
}
fetchProductData();
}, [categoryId, productId]);
if (loading) return <div>読み込み中...</div>;
if (!product || !category) return <div>商品が見つかりません</div>;
return (
<div className="product-detail">
<nav className="breadcrumb">
<Link to="/">ホーム</Link>
<span>/</span>
<Link to={`/categories/${categoryId}`}>{category.name}</Link>
<span>/</span>
<span>{product.name}</span>
</nav>
<div className="product-content">
<div className="product-images">
<img src={product.mainImage} alt={product.name} />
<div className="thumbnail-list">
{product.images.map((image, index) => (
<img key={index} src={image} alt={`${product.name} ${index + 1}`} />
))}
</div>
</div>
<div className="product-info">
<h1>{product.name}</h1>
<p className="price">¥{product.price.toLocaleString()}</p>
<p className="description">{product.description}</p>
<button className="btn btn-primary btn-large">
カートに追加
</button>
</div>
</div>
</div>
);
}
export default ProductDetail;
複数パラメータの取得
const { categoryId, productId } = useParams();
一度に複数のパラメータを取得できます。
パンくずナビゲーション
<nav className="breadcrumb">
<Link to="/">ホーム</Link>
<span>/</span>
<Link to={`/categories/${categoryId}`}>{category.name}</Link>
<span>/</span>
<span>{product.name}</span>
</nav>
取得したパラメータを使って、パンくずナビゲーションも作れます。
クエリパラメータの処理
useSearchParamsフックを使う
URLの?
以降のクエリパラメータは、useSearchParams
フックで処理できます。
検索機能やフィルタリング機能でよく使われます。
// components/ProductList.js
import React, { useState, useEffect } from 'react';
import { useSearchParams, Link } from 'react-router-dom';
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
// クエリパラメータから値を取得
const category = searchParams.get('category') || '';
const sortBy = searchParams.get('sort') || 'name';
const page = parseInt(searchParams.get('page')) || 1;
const searchQuery = searchParams.get('q') || '';
useEffect(() => {
async function fetchProducts() {
setLoading(true);
try {
const params = new URLSearchParams({
category,
sort: sortBy,
page: page.toString(),
q: searchQuery
});
const response = await fetch(`/api/products?${params}`);
const data = await response.json();
setProducts(data.products);
} catch (error) {
console.error('商品取得エラー:', error);
} finally {
setLoading(false);
}
}
fetchProducts();
}, [category, sortBy, page, searchQuery]);
// フィルターの更新
const updateFilter = (key, value) => {
const newParams = new URLSearchParams(searchParams);
if (value) {
newParams.set(key, value);
} else {
newParams.delete(key);
}
// ページは1にリセット
newParams.set('page', '1');
setSearchParams(newParams);
};
// ページ変更
const changePage = (newPage) => {
const newParams = new URLSearchParams(searchParams);
newParams.set('page', newPage.toString());
setSearchParams(newParams);
};
return (
<div className="product-list">
<div className="filters">
<div className="filter-group">
<label>検索:</label>
<input
type="text"
value={searchQuery}
onChange={(e) => updateFilter('q', e.target.value)}
placeholder="商品名で検索"
/>
</div>
<div className="filter-group">
<label>カテゴリ:</label>
<select
value={category}
onChange={(e) => updateFilter('category', e.target.value)}
>
<option value="">すべて</option>
<option value="electronics">電子機器</option>
<option value="clothing">衣類</option>
<option value="books">書籍</option>
</select>
</div>
<div className="filter-group">
<label>並び順:</label>
<select
value={sortBy}
onChange={(e) => updateFilter('sort', e.target.value)}
>
<option value="name">名前順</option>
<option value="price-asc">価格(安い順)</option>
<option value="price-desc">価格(高い順)</option>
<option value="date">新着順</option>
</select>
</div>
</div>
{loading ? (
<div className="loading">読み込み中...</div>
) : (
<>
<div className="product-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
<Pagination
currentPage={page}
onPageChange={changePage}
totalPages={Math.ceil(products.length / 12)}
/>
</>
)}
</div>
);
}
このコンポーネントのポイントを解説しますね。
クエリパラメータの取得
const category = searchParams.get('category') || '';
searchParams.get()
でクエリパラメータの値を取得できます。
URLの更新
const updateFilter = (key, value) => {
const newParams = new URLSearchParams(searchParams);
if (value) {
newParams.set(key, value);
} else {
newParams.delete(key);
}
setSearchParams(newParams);
};
フィルタが変更されると、URLのクエリパラメータも自動的に更新されます。
ページネーション
const changePage = (newPage) => {
const newParams = new URLSearchParams(searchParams);
newParams.set('page', newPage.toString());
setSearchParams(newParams);
};
ページ番号もクエリパラメータで管理します。
この仕組みにより、検索結果のURLをブックマークしたり、共有したりできるようになります!
ネストしたルート
基本的なネストルートの作り方
ネストしたルートを使うと、レイアウトを共有しながら複数のページを管理できます。 例えば、ダッシュボードのようなアプリでよく使われます。
// App.js - メインルート設定
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import DashboardLayout from './components/DashboardLayout';
import Dashboard from './components/Dashboard';
import Profile from './components/Profile';
import Settings from './components/Settings';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
{/* ネストしたルート */}
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<Dashboard />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
<Route path="analytics" element={<Analytics />} />
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
</BrowserRouter>
);
}
上記のコードでは、/dashboard
以下のルートをネストしています。
親ルート
<Route path="/dashboard" element={<DashboardLayout />}>
ダッシュボード全体のレイアウトを定義します。
子ルート
<Route index element={<Dashboard />} />
<Route path="profile" element={<Profile />} />
index
は親パスと同じURL(/dashboard
)を意味します。
path="profile"
は/dashboard/profile
になります。
ダッシュボードレイアウトの実装
ネストしたルートの親コンポーネントでは、Outlet
を使って子コンポーネントを表示します。
// components/DashboardLayout.js
import React from 'react';
import { Outlet, NavLink, useLocation } from 'react-router-dom';
function DashboardLayout() {
const location = useLocation();
return (
<div className="dashboard-layout">
<aside className="dashboard-sidebar">
<div className="sidebar-header">
<h2>ダッシュボード</h2>
</div>
<nav className="sidebar-nav">
<NavLink
to="/dashboard"
end
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
>
<span className="icon">📊</span>
概要
</NavLink>
<NavLink
to="/dashboard/profile"
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
>
<span className="icon">👤</span>
プロフィール
</NavLink>
<NavLink
to="/dashboard/settings"
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
>
<span className="icon">⚙️</span>
設定
</NavLink>
<NavLink
to="/dashboard/analytics"
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
>
<span className="icon">📈</span>
分析
</NavLink>
</nav>
</aside>
<main className="dashboard-main">
<header className="dashboard-header">
<h1>{getPageTitle(location.pathname)}</h1>
<div className="header-actions">
<button className="btn btn-primary">新規作成</button>
</div>
</header>
<div className="dashboard-content">
{/* ネストしたルートのコンポーネントがここに表示される */}
<Outlet />
</div>
</main>
</div>
);
}
function getPageTitle(pathname) {
const titles = {
'/dashboard': 'ダッシュボード概要',
'/dashboard/profile': 'プロフィール管理',
'/dashboard/settings': '設定',
'/dashboard/analytics': 'アナリティクス'
};
return titles[pathname] || 'ダッシュボード';
}
export default DashboardLayout;
このレイアウトコンポーネントのポイントを説明します。
Outlet
<Outlet />
子ルートのコンポーネントがここに表示されます。 これがネストルートの核心的な仕組みです。
NavLinkのend属性
<NavLink to="/dashboard" end>
end
属性をつけると、完全一致でのみアクティブになります。
これがないと、/dashboard/profile
でも/dashboard
のリンクがアクティブになってしまいます。
useLocationでページタイトル
const location = useLocation();
現在のパスを取得して、適切なページタイトルを表示します。
より複雑なネストルート
さらに複雑なネストルートの例も見てみましょう。
// 複数レベルのネスト
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/admin" element={<AdminLayout />}>
<Route index element={<AdminDashboard />} />
{/* ユーザー管理セクション */}
<Route path="users" element={<UsersLayout />}>
<Route index element={<UsersList />} />
<Route path="new" element={<CreateUser />} />
<Route path=":id" element={<UserDetail />} />
<Route path=":id/edit" element={<EditUser />} />
</Route>
{/* 商品管理セクション */}
<Route path="products" element={<ProductsLayout />}>
<Route index element={<ProductsList />} />
<Route path="new" element={<CreateProduct />} />
<Route path=":id" element={<ProductDetail />} />
<Route path=":id/edit" element={<EditProduct />} />
<Route path="categories" element={<CategoriesManagement />} />
</Route>
</Route>
</Routes>
</BrowserRouter>
);
}
この例では、管理画面内でさらにセクションごとにネストしています。
URLの対応関係
/admin
→ AdminDashboard/admin/users
→ UsersList/admin/users/new
→ CreateUser/admin/users/123
→ UserDetail(id=123)/admin/users/123/edit
→ EditUser(id=123)
これにより、階層的なページ構造を自然に表現できます。
ルートガードと認証
認証が必要なルートの保護
実際のアプリでは、ログインが必要なページがありますよね。 そういった場合は、ルートガードを実装して、未認証ユーザーをログインページにリダイレクトします。
// components/ProtectedRoute.js
import React from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
function ProtectedRoute({ children, requiredRole = null }) {
const { user, isAuthenticated, loading } = useAuth();
const location = useLocation();
// 認証状態をチェック中
if (loading) {
return <div className="loading">認証確認中...</div>;
}
// 未認証の場合、ログインページにリダイレクト
if (!isAuthenticated) {
return (
<Navigate
to="/login"
state={{ from: location }}
replace
/>
);
}
// 特定の役割が必要で、ユーザーがその役割を持たない場合
if (requiredRole && user.role !== requiredRole) {
return (
<Navigate
to="/unauthorized"
replace
/>
);
}
// 認証済みの場合、子コンポーネントを表示
return children;
}
export default ProtectedRoute;
このProtectedRouteコンポーネントの動作を説明しますね。
認証状態の確認
const { user, isAuthenticated, loading } = useAuth();
カスタムフックから認証状態を取得します。
未認証時のリダイレクト
<Navigate to="/login" state={{ from: location }} replace />
未認証の場合、ログインページにリダイレクトします。
state
でリダイレクト前のページを記録しておきます。
役割ベースのアクセス制御
if (requiredRole && user.role !== requiredRole) {
return <Navigate to="/unauthorized" replace />;
}
特定の役割が必要なページでは、ユーザーの役割もチェックします。
認証フックの実装
認証状態を管理するカスタムフックも実装しましょう。
// hooks/useAuth.js
import { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext();
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
// ページ読み込み時に認証状態を確認
checkAuthStatus();
}, []);
const checkAuthStatus = async () => {
try {
const token = localStorage.getItem('authToken');
if (!token) {
setLoading(false);
return;
}
const response = await fetch('/api/auth/verify', {
headers: {
Authorization: `Bearer ${token}`
}
});
if (response.ok) {
const userData = await response.json();
setUser(userData);
setIsAuthenticated(true);
} else {
localStorage.removeItem('authToken');
}
} catch (error) {
console.error('認証エラー:', error);
localStorage.removeItem('authToken');
} finally {
setLoading(false);
}
};
const login = async (credentials) => {
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
});
if (response.ok) {
const { user, token } = await response.json();
localStorage.setItem('authToken', token);
setUser(user);
setIsAuthenticated(true);
return { success: true };
} else {
const errorData = await response.json();
return { success: false, error: errorData.message };
}
} catch (error) {
return { success: false, error: 'ログインに失敗しました' };
}
};
const logout = () => {
localStorage.removeItem('authToken');
setUser(null);
setIsAuthenticated(false);
};
const value = {
user,
isAuthenticated,
loading,
login,
logout
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
このフックでは、以下の機能を提供しています。
認証状態の管理 ユーザー情報、認証状態、ローディング状態を管理します。
トークンの永続化 localStorageを使って、ページリロード後も認証状態を保持します。
ログイン・ログアウト機能 APIとやり取りして、認証を行います。
保護されたルートの設定
最後に、実際にルートを保護する設定を見てみましょう。
// App.js
import { AuthProvider } from './hooks/useAuth';
import ProtectedRoute from './components/ProtectedRoute';
function App() {
return (
<AuthProvider>
<BrowserRouter>
<Routes>
{/* パブリックルート */}
<Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
{/* 認証が必要なルート */}
<Route path="/dashboard" element={
<ProtectedRoute>
<DashboardLayout />
</ProtectedRoute>
}>
<Route index element={<Dashboard />} />
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
</Route>
{/* 管理者のみアクセス可能 */}
<Route path="/admin" element={
<ProtectedRoute requiredRole="admin">
<AdminLayout />
</ProtectedRoute>
}>
<Route index element={<AdminDashboard />} />
<Route path="users" element={<UsersManagement />} />
<Route path="settings" element={<AdminSettings />} />
</Route>
<Route path="/unauthorized" element={<UnauthorizedPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</BrowserRouter>
</AuthProvider>
);
}
この設定により、以下のような動作が実現されます。
- 未認証ユーザーが
/dashboard
にアクセス →/login
にリダイレクト - 一般ユーザーが
/admin
にアクセス →/unauthorized
にリダイレクト - 認証済みユーザーは対応するページに正常にアクセス
これで、セキュアなルーティングが実装できました!
プログラマティックナビゲーション
useNavigateフックでページ遷移
ユーザーのアクションに応じて、プログラムからページ遷移を行いたい場合があります。 例えば、フォーム送信後やボタンクリック時などです。
// components/LoginForm.js
import React, { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
function LoginForm() {
const [credentials, setCredentials] = useState({
email: '',
password: ''
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const location = useLocation();
const { login } = useAuth();
// ログイン前にアクセスしようとしていたページ
const from = location.state?.from?.pathname || '/dashboard';
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const result = await login(credentials);
if (result.success) {
// ログイン成功時、元のページまたはダッシュボードにリダイレクト
navigate(from, { replace: true });
} else {
setError(result.error);
}
} catch (err) {
setError('ログインに失敗しました');
} finally {
setLoading(false);
}
};
const handleInputChange = (e) => {
const { name, value } = e.target;
setCredentials(prev => ({
...prev,
[name]: value
}));
};
return (
<div className="login-form">
<h2>ログイン</h2>
{error && <div className="error-message">{error}</div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="email">メールアドレス</label>
<input
type="email"
id="email"
name="email"
value={credentials.email}
onChange={handleInputChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="password">パスワード</label>
<input
type="password"
id="password"
name="password"
value={credentials.password}
onChange={handleInputChange}
required
/>
</div>
<button
type="submit"
className="btn btn-primary"
disabled={loading}
>
{loading ? 'ログイン中...' : 'ログイン'}
</button>
</form>
<div className="form-footer">
<button
type="button"
className="link-button"
onClick={() => navigate('/register')}
>
アカウントをお持ちでない方はこちら
</button>
<button
type="button"
className="link-button"
onClick={() => navigate('/forgot-password')}
>
パスワードをお忘れの方はこちら
</button>
</div>
</div>
);
}
export default LoginForm;
このログインフォームのポイントを説明しますね。
元のページに戻る
const from = location.state?.from?.pathname || '/dashboard';
navigate(from, { replace: true });
ProtectedRouteからリダイレクトされた場合、ログイン後に元のページに戻ります。
replace: true ブラウザの履歴を置き換えるため、戻るボタンでログインページに戻らなくなります。
プログラムからのページ遷移
onClick={() => navigate('/register')}
ボタンクリックなどで、プログラムからページ遷移を行えます。
条件分岐のナビゲーション
複雑な条件に応じてナビゲーションを行う例も見てみましょう。
// components/ProductForm.js
import React, { useState, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
function ProductForm() {
const { id } = useParams();
const navigate = useNavigate();
const isEditMode = Boolean(id);
const [product, setProduct] = useState({
name: '',
price: '',
description: '',
category: ''
});
const [saving, setSaving] = useState(false);
useEffect(() => {
if (isEditMode) {
loadProduct(id);
}
}, [id, isEditMode]);
const loadProduct = async (productId) => {
try {
const response = await fetch(`/api/products/${productId}`);
const productData = await response.json();
setProduct(productData);
} catch (error) {
console.error('商品データの取得に失敗:', error);
navigate('/admin/products', {
state: { error: '商品データの取得に失敗しました' }
});
}
};
const handleSubmit = async (e) => {
e.preventDefault();
setSaving(true);
try {
const url = isEditMode
? `/api/products/${id}`
: '/api/products';
const method = isEditMode ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(product)
});
if (response.ok) {
const savedProduct = await response.json();
// 保存成功時の動作を分岐
if (isEditMode) {
// 編集モード:商品詳細ページに移動
navigate(`/admin/products/${savedProduct.id}`, {
state: { message: '商品を更新しました' }
});
} else {
// 新規作成モード:商品一覧に移動し、新規作成を続けるか確認
const continueCreating = window.confirm(
'商品を作成しました。続けて新しい商品を作成しますか?'
);
if (continueCreating) {
// フォームをリセットして新規作成を続行
setProduct({
name: '',
price: '',
description: '',
category: ''
});
} else {
// 商品一覧に移動
navigate('/admin/products', {
state: { message: '商品を作成しました' }
});
}
}
} else {
throw new Error('保存に失敗しました');
}
} catch (error) {
alert(`エラー: ${error.message}`);
} finally {
setSaving(false);
}
};
const handleCancel = () => {
const hasChanges = Object.values(product).some(value => value !== '');
if (hasChanges) {
const confirmLeave = window.confirm(
'変更が保存されていません。本当にページを離れますか?'
);
if (!confirmLeave) return;
}
// キャンセル時は前のページか商品一覧に戻る
if (isEditMode) {
navigate(`/admin/products/${id}`);
} else {
navigate('/admin/products');
}
};
return (
<div className="product-form">
<h2>{isEditMode ? '商品編集' : '商品作成'}</h2>
<form onSubmit={handleSubmit}>
{/* フォームの内容は省略 */}
<div className="form-actions">
<button
type="button"
onClick={handleCancel}
className="btn btn-secondary"
disabled={saving}
>
キャンセル
</button>
<button
type="submit"
className="btn btn-primary"
disabled={saving}
>
{saving ? '保存中...' : (isEditMode ? '更新' : '作成')}
</button>
</div>
</form>
</div>
);
}
export default ProductForm;
この例では、以下のような複雑なナビゲーション条件を実装しています。
編集モードと新規作成モードの分岐 URLパラメータの有無で、動作を切り替えています。
エラー時のナビゲーション データ取得に失敗した場合、エラーメッセージとともに一覧ページに戻ります。
保存後の分岐 編集時は詳細ページ、新規作成時は確認ダイアログで次の動作を決めています。
state を使った情報の受け渡し
navigate('/admin/products', {
state: { message: '商品を作成しました' }
});
ナビゲーション時に、メッセージなどの追加情報を渡せます。
よくあるつまずきポイントと対策
BrowserRouterの配置忘れ
初心者がよくやってしまう間違いを確認しましょう。
問題のあるコード
// ❌ BrowserRouter がない
function App() {
return (
<div>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</div>
);
}
// Error: useRoutes() may be used only in the context of a <Router> component.
ルーティング関連のコンポーネントを使うには、BrowserRouterが必要です。
修正版
// ✅ BrowserRouter で囲む
function App() {
return (
<BrowserRouter>
<div>
<Routes>
<Route path="/" element={<Home />} />
</Routes>
</div>
</BrowserRouter>
);
}
大切なポイント アプリの最上位でBrowserRouterを配置する必要があります。
ネストルートでのOutlet忘れ
ネストルートでよくある間違いです。
問題のあるコード
// ❌ Outlet がないため子ルートが表示されない
function Layout() {
return (
<div>
<nav>ナビゲーション</nav>
<main>
{/* 子ルートのコンポーネントが表示されない */}
</main>
</div>
);
}
子ルートを表示するには、Outletコンポーネントが必要です。
修正版
// ✅ Outlet を配置
import { Outlet } from 'react-router-dom';
function Layout() {
return (
<div>
<nav>ナビゲーション</nav>
<main>
<Outlet /> {/* 子ルートがここに表示される */}
</main>
</div>
);
}
覚えておくポイント ネストルートの親コンポーネントには、必ずOutletを配置しましょう。
useNavigateの依存関係エラー
useEffectでuseNavigateを使う際の注意点です。
問題のあるコード
// ❌ useEffect 内で navigate を使用するが依存配列に含めていない
function Component() {
const navigate = useNavigate();
useEffect(() => {
if (someCondition) {
navigate('/redirect');
}
}, [someCondition]); // navigate が依存配列にない
}
ESLintの警告が出る場合があります。
修正版
// ✅ navigate を依存配列に含める
function Component() {
const navigate = useNavigate();
useEffect(() => {
if (someCondition) {
navigate('/redirect');
}
}, [someCondition, navigate]);
// または useCallback を使用
const handleNavigation = useCallback(() => {
if (someCondition) {
navigate('/redirect');
}
}, [someCondition, navigate]);
useEffect(() => {
handleNavigation();
}, [handleNavigation]);
}
安全な書き方 navigateは安定した関数ですが、依存配列に含めることで警告を回避できます。
まとめ:Reactルーティングをマスターしよう
この記事では、Reactルーティングの基礎から応用まで詳しく解説しました。
今回学んだ重要なポイント
基本概念
- SPAでのルーティングの必要性
- React Router の役割と利点
- BrowserRouter、Routes、Route の基本構成
これらの基本を理解することで、Reactアプリの土台ができました。
実践的な機能
- 動的ルートとパラメータ(useParams)
- クエリパラメータの処理(useSearchParams)
- ネストしたルート(Outlet)
- プログラマティックナビゲーション(useNavigate)
これらの機能を組み合わせることで、複雑なナビゲーションも実現できます。
高度な機能
- ルートガードと認証
- 条件分岐ナビゲーション
- エラーハンドリング
実際のアプリケーションで必要な、セキュリティとユーザビリティを考慮した機能です。
今日から始められること
以下の順番で取り組むことをおすすめします。
1. React Router のインストール
npm install react-router-dom
2. 基本的なルート設定 BrowserRouter、Routes、Routeを使った簡単な設定から始めましょう。
3. ナビゲーションコンポーネントの作成 LinkやNavLinkを使って、使いやすいナビゲーションを作成しましょう。
4. 動的ルートの実装 useParamsを使って、パラメータを受け取るページを作成しましょう。
Reactのルーティングは最初は複雑に感じるかもしれませんが、基本を理解すれば様々な機能を組み合わせて豊富なナビゲーション体験を作れます。
ユーザーフレンドリーなSPAを作るために、ぜひReact Routerを活用してみてください! きっと、あなたのReactスキルが大きく向上するはずです。