Reactルーティング入門|ページ遷移の仕組みを基礎から解説

React Routerを使ったページ遷移の基礎から応用まで詳しく解説。ルーティングの概念、実装方法、動的ルート、ネストしたルートまで実践的に学べます。

Learning Next 運営
70 分で読めます

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;

ここでは、LinkNavLinkという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で、id123が入ります。

: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スキルが大きく向上するはずです。

関連記事