React Routerの基本|シンプルなページ遷移から始める実装ガイド

React Routerの基本的な使い方から実装方法まで初心者向けに詳しく解説。ルーティング設定やページ遷移、パラメータ取得など実践的な例とともに学べます。

Learning Next 運営
83 分で読めます

みなさん、Reactでアプリケーションを作っていて「複数のページを作りたい」と思ったことはありませんか?

「ユーザーがURLを変更したときに、画面を切り替えたい」 「ブラウザの戻るボタンで前のページに戻りたい」 「URLを直接入力して特定のページにアクセスしたい」

このように感じたことはありませんか?

従来のHTMLサイトでは、ページごとにHTMLファイルを作成していました。 しかし、ReactのようなSPA(Single Page Application)では、JavaScript上でページ遷移を管理する必要があります。

この記事では、React Routerの基本的な使い方から実装方法まで初心者向けに詳しく解説します。 ルーティング設定やページ遷移、パラメータ取得など実践的な例とともに学んでいきましょう。

React Routerとは

SPAにおけるルーティングの必要性

React Routerは、React アプリケーションでページ遷移を管理するためのライブラリです。

簡単に言うと、URLの変更に応じて異なるコンポーネントを表示する仕組みを提供してくれます。

従来のWebサイトとSPAの違い

まず、従来のWebサイトでのページ遷移を見てみましょう。

<!-- 従来のWebサイト -->
<a href="/about.html">About</a>
<a href="/contact.html">Contact</a>

従来のWebサイトでは、リンクをクリックするとサーバーから新しいHTMLファイルを読み込みます。 この方法だと、ページが切り替わるたびに画面が白くなって読み込まれますよね。

一方、ReactのSPAでは、以下のような方法でページを切り替えます:

// SPA(React)でのページ遷移
const 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')}>Home</button>
        <button onClick={() => setCurrentPage('about')}>About</button>
        <button onClick={() => setCurrentPage('contact')}>Contact</button>
      </nav>
      {renderPage()}
    </div>
  );
};

このコードでは、状態(currentPage)を変更することでページを切り替えています。

しかし、この方法では以下の問題があります。

素のReactでの問題点

素のReactだけでページ遷移を実装すると、いくつか困った問題が発生します:

// 問題1: URLが変わらない
// ユーザーがページをリロードすると、常にホームページが表示される

// 問題2: ブラウザの戻る・進むボタンが使えない
// ユーザーの期待する動作と異なる

// 問題3: 直接URLアクセスができない
// 例:https://example.com/about にアクセスしても意味がない

// 問題4: SEO的に不利
// 検索エンジンが各ページを認識できない

これらの問題があると、ユーザーにとって使いにくいアプリケーションになってしまいます。

React Routerは、これらの問題を解決してくれます。 つまり、普通のWebサイトと同じようにURLでページを管理できるようになるんです。

React Routerの基本概念

React Routerは、以下の仕組みで動作します。

ブラウザの履歴管理

React Routerは、ブラウザの履歴API(History API)を使用してURLを管理します。

// ブラウザの履歴APIを使用
// history.pushState(), history.replaceState()
// popstateイベント(戻る・進むボタン)

const Navigation = () => {
  const navigate = useNavigate();
  
  const handleNavigation = (path) => {
    navigate(path); // ブラウザの履歴に追加
  };
  
  return (
    <nav>
      <button onClick={() => handleNavigation('/')}>Home</button>
      <button onClick={() => handleNavigation('/about')}>About</button>
    </nav>
  );
};

この例では、navigate関数を使ってページを切り替えています。 URLの変更とコンポーネントの表示を同期させることで、自然なページ遷移を実現します。

ルートマッチング

React Routerは、現在のURLパスと定義されたルートを照合して、表示するコンポーネントを決定します。

// URLパスとコンポーネントの対応
const routeConfig = {
  '/': HomePage,
  '/about': AboutPage,
  '/contact': ContactPage,
  '/users/:id': UserPage, // 動的パラメータ
  '/products/:category/:id': ProductPage // 複数パラメータ
};

このような対応表を元に、現在のURLパスに基づいて適切なコンポーネントを表示します。

React Routerの種類と選び方

React Routerには複数のパッケージがあります。

主要なパッケージ

用途に応じて、適切なパッケージを選択します:

# React Router DOM(Webアプリケーション用)
npm install react-router-dom

# React Router Native(React Native用)
npm install react-router-native

# React Router Core(基本機能のみ)
npm install react-router

一般的なWebアプリケーションでは、react-router-domを使用します。 これが最も一般的で、必要な機能がすべて含まれています。

バージョンの違い

React Routerは、バージョンによって書き方が異なります:

// React Router v5 以前
import { BrowserRouter, Switch, Route } from 'react-router-dom';

// React Router v6 以降(現在の推奨)
import { BrowserRouter, Routes, Route } from 'react-router-dom';

この記事では、最新版のReact Router v6を使用して解説します。 v6の方が書きやすく、機能も豊富なので、新しいプロジェクトではv6をおすすめします。

基本的な設定と使い方

インストールと初期設定

React Routerを使用する準備から始めましょう。

パッケージのインストール

まず、プロジェクトにReact Router DOMをインストールします:

# プロジェクトディレクトリで実行
npm install react-router-dom

# または yarn を使用
yarn add react-router-dom

このコマンドを実行すると、依存関係としてReact Router DOM がインストールされます。

基本的なルーター設定

インストールが完了したら、アプリケーションにルーターを設定しましょう:

// src/App.js
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';
import ContactPage from './pages/ContactPage';

const App = () => {
  return (
    <BrowserRouter>
      <div className="app">
        <Routes>
          <Route path="/" element={<HomePage />} />
          <Route path="/about" element={<AboutPage />} />
          <Route path="/contact" element={<ContactPage />} />
        </Routes>
      </div>
    </BrowserRouter>
  );
};

export default App;

このコードの仕組みを詳しく見てみましょう。

BrowserRouterコンポーネント

<BrowserRouter>
  {/* アプリケーション全体をラップ */}
</BrowserRouter>

BrowserRouterは、アプリケーション全体をラップして、ルーティング機能を提供します。 これがないと、他のルーター関連のコンポーネントが動作しません。

RoutesとRouteコンポーネント

<Routes>
  <Route path="/" element={<HomePage />} />
  <Route path="/about" element={<AboutPage />} />
  <Route path="/contact" element={<ContactPage />} />
</Routes>

Routesは複数のRouteをまとめるコンテナです。 Routeは、特定のパス(path)に対して表示するコンポーネント(element)を定義します。

基本的なページコンポーネント

各ページに対応するコンポーネントを作成しましょう:

// src/pages/HomePage.js
const HomePage = () => {
  return (
    <div>
      <h1>ホームページ</h1>
      <p>当サイトへようこそ!</p>
    </div>
  );
};

export default HomePage;
// src/pages/AboutPage.js
const AboutPage = () => {
  return (
    <div>
      <h1>私たちについて</h1>
      <p>私たちのミッションとビジョンを紹介します。</p>
    </div>
  );
};

export default AboutPage;
// src/pages/ContactPage.js
const ContactPage = () => {
  return (
    <div>
      <h1>お問い合わせ</h1>
      <p>ご質問やご要望がございましたら、お気軽にお問い合わせください。</p>
    </div>
  );
};

export default ContactPage;

各ページは独立したコンポーネントとして作成します。 ファイルを分けることで、コードが整理されて管理しやすくなりますよ。

Link コンポーネントによるナビゲーション

ページ間の移動にはLinkコンポーネントを使用します。

基本的なナビゲーション

まず、基本的なナビゲーションを作成してみましょう:

// src/components/Navigation.js
import { Link } from 'react-router-dom';

const Navigation = () => {
  return (
    <nav className="navigation">
      <ul>
        <li>
          <Link to="/">ホーム</Link>
        </li>
        <li>
          <Link to="/about">私たちについて</Link>
        </li>
        <li>
          <Link to="/contact">お問い合わせ</Link>
        </li>
      </ul>
    </nav>
  );
};

export default Navigation;

このNavigationコンポーネントを詳しく見てみましょう。

Linkコンポーネントの特徴

<Link to="/about">私たちについて</Link>

Linkコンポーネントは、ページリロードなしでルート遷移を行います。 普通の<a>タグとは違って、SPAの利点を活かした高速なページ遷移が可能です。

ナビゲーションの統合

作成したナビゲーションを、アプリケーション全体に組み込みましょう:

// src/App.js
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Navigation from './components/Navigation';
import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';
import ContactPage from './pages/ContactPage';

const App = () => {
  return (
    <BrowserRouter>
      <div className="app">
        <Navigation />
        <main className="main-content">
          <Routes>
            <Route path="/" element={<HomePage />} />
            <Route path="/about" element={<AboutPage />} />
            <Route path="/contact" element={<ContactPage />} />
          </Routes>
        </main>
      </div>
    </BrowserRouter>
  );
};

export default App;

この構成により、ナビゲーションコンポーネントをRoutes の外側に配置することで、全ページで共有できます。 ページが切り替わっても、ナビゲーションは常に表示され続けます。

アクティブなリンクのスタイリング

現在のページに対応するリンクを強調表示する方法です:

// src/components/Navigation.js
import { NavLink } from 'react-router-dom';

const Navigation = () => {
  return (
    <nav className="navigation">
      <ul>
        <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>
  );
};

NavLinkの特徴

<NavLink 
  to="/about" 
  className={({ isActive }) => isActive ? 'active' : ''}
>
  私たちについて
</NavLink>

NavLinkは、現在のページと一致する場合にisActiveがtrueになります。 これを使って、現在のページを視覚的に分かりやすくできます。

対応するCSS

.navigation .active {
  color: #007bff;
  font-weight: bold;
  border-bottom: 2px solid #007bff;
}

このCSSを追加することで、現在のページに対応するリンクが強調表示されます。

プログラムによる遷移

ボタンクリックやフォーム送信など、プログラムでページ遷移を行う方法です。

useNavigate フックの使用

useNavigateフックを使うと、JavaScript でページ遷移を制御できます:

// src/pages/HomePage.js
import { useNavigate } from 'react-router-dom';

const HomePage = () => {
  const navigate = useNavigate();
  
  const handleGetStarted = () => {
    // 何らかの処理を実行
    console.log('ユーザーが開始ボタンをクリックしました');
    
    // プログラムでページ遷移
    navigate('/about');
  };
  
  const handleContactUs = () => {
    navigate('/contact');
  };
  
  return (
    <div className="home-page">
      <h1>ホームページ</h1>
      <p>当サイトへようこそ!</p>
      
      <div className="action-buttons">
        <button onClick={handleGetStarted}>
          始めてみる
        </button>
        <button onClick={handleContactUs}>
          お問い合わせ
        </button>
      </div>
    </div>
  );
};

export default HomePage;

この例では、ボタンをクリックすると指定したページに遷移します。

useNavigateの使い方

const navigate = useNavigate();

const handleNavigation = () => {
  navigate('/about'); // 指定したパスに遷移
};

useNavigateフックを使用することで、JavaScript でページ遷移を制御できます。 フォームの送信後や、何らかの処理が完了した後に自動でページを切り替えたい場合に便利です。

条件付きナビゲーション

ユーザーの状態や条件に応じて、異なるページへ遷移させることもできます:

// src/components/UserProfile.js
import { useNavigate } from 'react-router-dom';

const UserProfile = ({ user }) => {
  const navigate = useNavigate();
  
  const handleEditProfile = () => {
    if (user.isAuthenticated) {
      navigate('/profile/edit');
    } else {
      // ログインしていない場合はログインページへ
      navigate('/login');
    }
  };
  
  const handleDeleteAccount = () => {
    if (window.confirm('アカウントを削除しますか?')) {
      // アカウント削除処理
      deleteUserAccount();
      
      // ホームページへリダイレクト
      navigate('/', { replace: true });
    }
  };
  
  return (
    <div className="user-profile">
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      
      <div className="profile-actions">
        <button onClick={handleEditProfile}>
          プロフィール編集
        </button>
        <button onClick={handleDeleteAccount}>
          アカウント削除
        </button>
      </div>
    </div>
  );
};

この例では、ユーザーの認証状態に応じて遷移先を変更しています。

履歴の操作

ブラウザの履歴を操作することもできます:

// src/components/BackButton.js
import { useNavigate } from 'react-router-dom';

const BackButton = () => {
  const navigate = useNavigate();
  
  const handleBack = () => {
    navigate(-1); // 前のページに戻る
  };
  
  const handleForward = () => {
    navigate(1); // 次のページに進む
  };
  
  const handleGoHome = () => {
    navigate('/', { replace: true }); // 履歴を置き換え
  };
  
  return (
    <div className="navigation-controls">
      <button onClick={handleBack}>
        ← 戻る
      </button>
      <button onClick={handleForward}>
        進む →
      </button>
      <button onClick={handleGoHome}>
        ホームに戻る
      </button>
    </div>
  );
};

navigateのオプション

navigate(-1); // 前のページに戻る
navigate(1);  // 次のページに進む
navigate('/', { replace: true }); // 履歴を置き換えて遷移

navigateに数値を渡すことで、履歴を操作できます。 replace: trueオプションを使うと、現在のページを履歴から削除して遷移します。

パラメータと動的ルーティング

URL パラメータの取得

動的なURLパラメータを使用して、データベースのIDやカテゴリなどに基づいてページを表示する方法です。

基本的なパラメータ設定

まず、動的パラメータを含むルートを定義しましょう:

// src/App.js
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import UserPage from './pages/UserPage';
import ProductPage from './pages/ProductPage';

const App = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/users/:id" element={<UserPage />} />
        <Route path="/products/:category/:id" element={<ProductPage />} />
      </Routes>
    </BrowserRouter>
  );
};

このルート定義を詳しく見てみましょう。

動的パラメータの書き方

<Route path="/users/:id" element={<UserPage />} />
<Route path="/products/:category/:id" element={<ProductPage />} />

:id:categoryのような記法で、動的パラメータを定義します。 これにより、/users/123/products/electronics/456のようなURLにマッチします。

useParams フックでパラメータ取得

動的パラメータを取得するには、useParamsフックを使用します:

// src/pages/UserPage.js
import { useParams } from 'react-router-dom';
import { useState, useEffect } from 'react';

const UserPage = () => {
  const { id } = useParams();
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/users/${id}`);
        const userData = await response.json();
        setUser(userData);
      } catch (error) {
        console.error('ユーザー情報の取得に失敗しました:', error);
      } finally {
        setLoading(false);
      }
    };
    
    fetchUser();
  }, [id]);
  
  if (loading) {
    return <div>ユーザー情報を読み込み中...</div>;
  }
  
  if (!user) {
    return <div>ユーザーが見つかりません。</div>;
  }
  
  return (
    <div className="user-page">
      <h1>{user.name}</h1>
      <p>ID: {id}</p>
      <p>メール: {user.email}</p>
      <p>部署: {user.department}</p>
    </div>
  );
};

export default UserPage;

このUserPageコンポーネントを詳しく見てみましょう。

useParamsフックの使い方

const { id } = useParams();

useParamsフックを使用することで、URL パラメータを簡単に取得できます。 URLが/users/123の場合、idには"123"が入ります。

API呼び出しでのパラメータ活用

useEffect(() => {
  const fetchUser = async () => {
    const response = await fetch(`/api/users/${id}`);
    // ...
  };
  
  fetchUser();
}, [id]);

取得したパラメータを使って、対応するデータをAPIから取得しています。 idが変更されるたびに、新しいデータを取得し直します。

複数パラメータの処理

複数のパラメータがある場合の処理例です:

// src/pages/ProductPage.js
import { useParams } from 'react-router-dom';
import { useState, useEffect } from 'react';

const ProductPage = () => {
  const { category, id } = useParams();
  const [product, setProduct] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const fetchProduct = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/products/${category}/${id}`);
        const productData = await response.json();
        setProduct(productData);
      } catch (error) {
        console.error('商品情報の取得に失敗しました:', error);
      } finally {
        setLoading(false);
      }
    };
    
    fetchProduct();
  }, [category, id]);
  
  if (loading) {
    return <div>商品情報を読み込み中...</div>;
  }
  
  if (!product) {
    return <div>商品が見つかりません。</div>;
  }
  
  return (
    <div className="product-page">
      <nav className="breadcrumb">
        <span>カテゴリ: {category}</span>
        <span> / </span>
        <span>商品ID: {id}</span>
      </nav>
      
      <h1>{product.name}</h1>
      <p className="price">価格: ¥{product.price}</p>
      <p className="description">{product.description}</p>
      
      <div className="product-actions">
        <button>カートに追加</button>
        <button>お気に入り</button>
      </div>
    </div>
  );
};

export default ProductPage;

複数パラメータの取得

const { category, id } = useParams();

複数のパラメータを同時に取得できます。 URLが/products/electronics/123の場合、category"electronics"id"123"が入ります。

依存配列での複数パラメータ管理

useEffect(() => {
  fetchProduct();
}, [category, id]);

複数のパラメータが変更された場合に、それぞれ新しいデータを取得するよう設定しています。

クエリパラメータの処理

URL のクエリパラメータ(?key=value)を処理する方法です。

useSearchParams フックの使用

クエリパラメータを扱うには、useSearchParamsフックを使用します:

// src/pages/SearchPage.js
import { useSearchParams } from 'react-router-dom';
import { useState, useEffect } from 'react';

const SearchPage = () => {
  const [searchParams, setSearchParams] = useSearchParams();
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  
  // URL からクエリパラメータを取得
  const query = searchParams.get('q') || '';
  const category = searchParams.get('category') || 'all';
  const page = parseInt(searchParams.get('page')) || 1;
  
  useEffect(() => {
    if (query) {
      performSearch(query, category, page);
    }
  }, [query, category, page]);
  
  const performSearch = async (searchQuery, searchCategory, searchPage) => {
    setLoading(true);
    try {
      const response = await fetch(
        `/api/search?q=${encodeURIComponent(searchQuery)}&category=${searchCategory}&page=${searchPage}`
      );
      const data = await response.json();
      setResults(data.results);
    } catch (error) {
      console.error('検索に失敗しました:', error);
    } finally {
      setLoading(false);
    }
  };
  
  const handleCategoryChange = (newCategory) => {
    setSearchParams({
      q: query,
      category: newCategory,
      page: 1
    });
  };
  
  const handlePageChange = (newPage) => {
    setSearchParams({
      q: query,
      category: category,
      page: newPage
    });
  };
  
  return (
    <div className="search-page">
      <h1>検索結果</h1>
      
      <div className="search-controls">
        <p>検索キーワード: "{query}"</p>
        
        <div className="category-filter">
          <label>カテゴリ:</label>
          <select 
            value={category} 
            onChange={(e) => handleCategoryChange(e.target.value)}
          >
            <option value="all">すべて</option>
            <option value="electronics">電子機器</option>
            <option value="clothing">衣類</option>
            <option value="books">書籍</option>
          </select>
        </div>
      </div>
      
      {loading ? (
        <div>検索中...</div>
      ) : (
        <div className="search-results">
          {results.map(item => (
            <div key={item.id} className="search-result-item">
              <h3>{item.title}</h3>
              <p>{item.description}</p>
            </div>
          ))}
        </div>
      )}
      
      <div className="pagination">
        <button 
          disabled={page <= 1}
          onClick={() => handlePageChange(page - 1)}
        >
          前のページ
        </button>
        <span>ページ {page}</span>
        <button 
          onClick={() => handlePageChange(page + 1)}
        >
          次のページ
        </button>
      </div>
    </div>
  );
};

export default SearchPage;

このSearchPageコンポーネントを詳しく見てみましょう。

useSearchParamsの基本的な使い方

const [searchParams, setSearchParams] = useSearchParams();

const query = searchParams.get('q') || '';
const category = searchParams.get('category') || 'all';

useSearchParamsを使用することで、クエリパラメータを簡単に読み取れます。 searchParams.get('q')で、URLの?q=キーワードの値を取得できます。

クエリパラメータの更新

const handleCategoryChange = (newCategory) => {
  setSearchParams({
    q: query,
    category: newCategory,
    page: 1
  });
};

setSearchParamsでクエリパラメータを更新できます。 URLが自動的に変更され、ブラウザの履歴にも追加されます。

検索フォームとの連携

検索フォームからクエリパラメータ付きのページに遷移する例です:

// src/components/SearchForm.js
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

const SearchForm = () => {
  const [query, setQuery] = useState('');
  const [category, setCategory] = useState('all');
  const navigate = useNavigate();
  
  const handleSubmit = (e) => {
    e.preventDefault();
    
    if (query.trim()) {
      // 検索ページへ遷移(クエリパラメータ付き)
      navigate(`/search?q=${encodeURIComponent(query)}&category=${category}`);
    }
  };
  
  return (
    <form onSubmit={handleSubmit} className="search-form">
      <div className="form-group">
        <input
          type="text"
          placeholder="検索キーワードを入力"
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          className="search-input"
        />
        
        <select 
          value={category} 
          onChange={(e) => setCategory(e.target.value)}
          className="category-select"
        >
          <option value="all">すべて</option>
          <option value="electronics">電子機器</option>
          <option value="clothing">衣類</option>
          <option value="books">書籍</option>
        </select>
        
        <button type="submit" className="search-button">
          検索
        </button>
      </div>
    </form>
  );
};

export default SearchForm;

フォーム送信時のクエリパラメータ生成

const handleSubmit = (e) => {
  e.preventDefault();
  
  if (query.trim()) {
    navigate(`/search?q=${encodeURIComponent(query)}&category=${category}`);
  }
};

フォームの送信時に、入力値をクエリパラメータとしてURLに含めて遷移しています。 encodeURIComponentで、日本語などの文字を適切にエンコードしています。

オプショナルパラメータ

必須ではないパラメータを扱う方法です。

複数のルートパターン

同じコンポーネントで、異なるパラメータパターンを処理できます:

// src/App.js
const App = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/blog" element={<BlogPage />} />
        <Route path="/blog/:category" element={<BlogPage />} />
        <Route path="/blog/:category/:id" element={<BlogPostPage />} />
      </Routes>
    </BrowserRouter>
  );
};

この設定により、以下のURLパターンに対応できます:

  • /blog - すべての記事
  • /blog/tech - techカテゴリの記事一覧
  • /blog/tech/123 - 特定の記事の詳細

パラメータの有無に応じた処理

パラメータがある場合とない場合で、異なる処理を行う例です:

// src/pages/BlogPage.js
import { useParams } from 'react-router-dom';
import { useState, useEffect } from 'react';

const BlogPage = () => {
  const { category } = useParams();
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const fetchPosts = async () => {
      try {
        setLoading(true);
        const url = category 
          ? `/api/blog/posts?category=${category}`
          : '/api/blog/posts';
        
        const response = await fetch(url);
        const data = await response.json();
        setPosts(data);
      } catch (error) {
        console.error('記事の取得に失敗しました:', error);
      } finally {
        setLoading(false);
      }
    };
    
    fetchPosts();
  }, [category]);
  
  return (
    <div className="blog-page">
      <h1>
        {category ? `${category} の記事` : 'すべての記事'}
      </h1>
      
      {loading ? (
        <div>記事を読み込み中...</div>
      ) : (
        <div className="blog-posts">
          {posts.map(post => (
            <article key={post.id} className="blog-post-preview">
              <h2>{post.title}</h2>
              <p>{post.excerpt}</p>
              <p className="post-meta">
                カテゴリ: {post.category} | 
                投稿日: {new Date(post.createdAt).toLocaleDateString()}
              </p>
            </article>
          ))}
        </div>
      )}
    </div>
  );
};

export default BlogPage;

条件分岐による処理の変更

const url = category 
  ? `/api/blog/posts?category=${category}`
  : '/api/blog/posts';

パラメータの有無に応じて、異なるAPIエンドポイントを呼び出します。 これにより、1つのコンポーネントで複数のパターンに対応できます。

ネストされたルーティング

子ルートの設定

複雑なアプリケーションでは、ページ内に複数のサブページを持つことがあります。

基本的なネストルート

ネストルートを使って、階層構造を表現してみましょう:

// src/App.js
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import DashboardLayout from './pages/DashboardLayout';
import DashboardHome from './pages/DashboardHome';
import DashboardProfile from './pages/DashboardProfile';
import DashboardSettings from './pages/DashboardSettings';

const App = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/dashboard" element={<DashboardLayout />}>
          <Route index element={<DashboardHome />} />
          <Route path="profile" element={<DashboardProfile />} />
          <Route path="settings" element={<DashboardSettings />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
};

この設定により、以下のURLパターンに対応できます:

  • /dashboard - ダッシュボードのホーム
  • /dashboard/profile - プロフィールページ
  • /dashboard/settings - 設定ページ

ネストルートの書き方

<Route path="/dashboard" element={<DashboardLayout />}>
  <Route index element={<DashboardHome />} />
  <Route path="profile" element={<DashboardProfile />} />
</Route>

Routeコンポーネントをネストすることで、階層構造を表現できます。 indexは、親ルートと同じパスの場合に表示されるデフォルトのコンポーネントです。

親レイアウトコンポーネント

親レイアウトコンポーネントで、共通のレイアウトを定義します:

// src/pages/DashboardLayout.js
import { Outlet, Link } from 'react-router-dom';

const DashboardLayout = () => {
  return (
    <div className="dashboard-layout">
      <header className="dashboard-header">
        <h1>ダッシュボード</h1>
      </header>
      
      <div className="dashboard-content">
        <nav className="dashboard-sidebar">
          <ul>
            <li>
              <Link to="/dashboard">ホーム</Link>
            </li>
            <li>
              <Link to="/dashboard/profile">プロフィール</Link>
            </li>
            <li>
              <Link to="/dashboard/settings">設定</Link>
            </li>
          </ul>
        </nav>
        
        <main className="dashboard-main">
          {/* 子ルートがここに表示される */}
          <Outlet />
        </main>
      </div>
    </div>
  );
};

export default DashboardLayout;

Outletコンポーネントの役割

<main className="dashboard-main">
  <Outlet />
</main>

Outletコンポーネントが、子ルートの内容を表示する場所を指定します。 現在のURLに応じて、適切な子コンポーネントがここに表示されます。

子ルートコンポーネント

各子ルートに対応するコンポーネントを作成します:

// src/pages/DashboardHome.js
const DashboardHome = () => {
  return (
    <div className="dashboard-home">
      <h2>ダッシュボードホーム</h2>
      <p>最近のアクティビティや重要な情報を表示します。</p>
      
      <div className="dashboard-stats">
        <div className="stat-card">
          <h3>今日の訪問者</h3>
          <p>1,234人</p>
        </div>
        <div className="stat-card">
          <h3>新規登録</h3>
          <p>45人</p>
        </div>
      </div>
    </div>
  );
};

export default DashboardHome;
// src/pages/DashboardProfile.js
const DashboardProfile = () => {
  return (
    <div className="dashboard-profile">
      <h2>プロフィール</h2>
      <p>ユーザーのプロフィール情報を表示・編集します。</p>
      
      <form className="profile-form">
        <div className="form-group">
          <label>名前</label>
          <input type="text" defaultValue="田中太郎" />
        </div>
        <div className="form-group">
          <label>メールアドレス</label>
          <input type="email" defaultValue="tanaka@example.com" />
        </div>
        <button type="submit">保存</button>
      </form>
    </div>
  );
};

export default DashboardProfile;

各子ルートは独立したコンポーネントとして実装します。 親レイアウトの中で表示されるため、ヘッダーやサイドバーは共通で使えます。

複数レベルのネスト

さらに深い階層のルーティングも可能です。

深いネスト構造

管理画面のような複雑な構造の例です:

// src/App.js
const App = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/admin" element={<AdminLayout />}>
          <Route index element={<AdminDashboard />} />
          <Route path="users" element={<UsersLayout />}>
            <Route index element={<UsersList />} />
            <Route path=":id" element={<UserDetail />} />
            <Route path=":id/edit" element={<UserEdit />} />
          </Route>
          <Route path="products" element={<ProductsLayout />}>
            <Route index element={<ProductsList />} />
            <Route path="new" element={<ProductNew />} />
            <Route path=":id" element={<ProductDetail />} />
            <Route path=":id/edit" element={<ProductEdit />} />
          </Route>
        </Route>
      </Routes>
    </BrowserRouter>
  );
};

この設定により、以下のようなURLパターンに対応できます:

  • /admin - 管理ダッシュボード
  • /admin/users - ユーザー一覧
  • /admin/users/123 - ユーザー詳細
  • /admin/users/123/edit - ユーザー編集
  • /admin/products - 商品一覧
  • /admin/products/new - 新規商品作成

中間レイアウトコンポーネント

セクションごとの共通レイアウトを定義できます:

// src/pages/admin/UsersLayout.js
import { Outlet, Link } from 'react-router-dom';

const UsersLayout = () => {
  return (
    <div className="users-layout">
      <div className="users-header">
        <h2>ユーザー管理</h2>
        <nav className="users-nav">
          <Link to="/admin/users">ユーザー一覧</Link>
          <Link to="/admin/users/new">新規ユーザー</Link>
        </nav>
      </div>
      
      <div className="users-content">
        <Outlet />
      </div>
    </div>
  );
};

export default UsersLayout;

中間レイアウトコンポーネントにより、セクションごとの共通レイアウトを定義できます。 これにより、一貫性のあるUIを保ちながら、機能ごとに整理された構造を作れます。

相対パスとインデックスルート

ネストルートでの相対パスの使用方法です。

相対パスナビゲーション

相対パスを使うと、階層構造を意識した直感的なナビゲーションが可能です:

// src/components/UserNavigation.js
import { Link, useParams } from 'react-router-dom';

const UserNavigation = () => {
  const { id } = useParams();
  
  return (
    <nav className="user-navigation">
      <Link to=".">ユーザー詳細</Link>
      <Link to="edit">編集</Link>
      <Link to="posts">投稿一覧</Link>
      <Link to="settings">設定</Link>
    </nav>
  );
};

相対パスの動作

// 使用例:
// /admin/users/123 にいる場合
// "." → /admin/users/123
// "edit" → /admin/users/123/edit
// "posts" → /admin/users/123/posts

相対パスを使用することで、現在の位置を基準とした自然なナビゲーションができます。

インデックスルートの活用

デフォルトで表示するコンテンツを指定する方法です:

// src/pages/UserDetail.js
import { Outlet, Link } from 'react-router-dom';

const UserDetail = () => {
  const { id } = useParams();
  
  return (
    <div className="user-detail">
      <h2>ユーザー詳細</h2>
      
      <nav className="user-tabs">
        <Link to=".">基本情報</Link>
        <Link to="posts">投稿履歴</Link>
        <Link to="activity">アクティビティ</Link>
      </nav>
      
      <div className="user-content">
        <Outlet />
      </div>
    </div>
  );
};

// ルート設定
<Route path="users/:id" element={<UserDetail />}>
  <Route index element={<UserBasicInfo />} />
  <Route path="posts" element={<UserPosts />} />
  <Route path="activity" element={<UserActivity />} />
</Route>

インデックスルートの特徴

<Route index element={<UserBasicInfo />} />

インデックスルートにより、親ルートにアクセスしたときのデフォルトコンテンツを指定できます。 /admin/users/123にアクセスすると、UserBasicInfoコンポーネントが表示されます。

実践的な活用例

認証が必要なページの保護

ログインが必要なページを保護する方法です。

認証コンテキストの作成

まず、認証状態を管理するコンテキストを作成しましょう:

// src/contexts/AuthContext.js
import { createContext, useContext, useState, useEffect } from 'react';

const AuthContext = createContext();

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider');
  }
  return context;
};

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    const checkAuth = async () => {
      try {
        const token = localStorage.getItem('authToken');
        if (token) {
          const response = await fetch('/api/auth/me', {
            headers: { Authorization: `Bearer ${token}` }
          });
          if (response.ok) {
            const userData = await response.json();
            setUser(userData);
          }
        }
      } catch (error) {
        console.error('認証チェックに失敗:', error);
      } finally {
        setLoading(false);
      }
    };
    
    checkAuth();
  }, []);
  
  const login = async (email, password) => {
    try {
      const response = await fetch('/api/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password })
      });
      
      if (response.ok) {
        const { user, token } = await response.json();
        localStorage.setItem('authToken', token);
        setUser(user);
        return { success: true };
      } else {
        return { success: false, error: 'ログインに失敗しました' };
      }
    } catch (error) {
      return { success: false, error: 'ネットワークエラーが発生しました' };
    }
  };
  
  const logout = () => {
    localStorage.removeItem('authToken');
    setUser(null);
  };
  
  return (
    <AuthContext.Provider value={{ user, login, logout, loading }}>
      {children}
    </AuthContext.Provider>
  );
};

このAuthContextでは、ユーザーの認証状態とログイン・ログアウト機能を管理しています。

保護されたルートコンポーネント

認証が必要なページをラップするコンポーネントを作成します:

// src/components/ProtectedRoute.js
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';

const ProtectedRoute = ({ children }) => {
  const { user, loading } = useAuth();
  const location = useLocation();
  
  if (loading) {
    return <div>認証状態を確認中...</div>;
  }
  
  if (!user) {
    // ログインしていない場合、ログインページへリダイレクト
    // 現在のページを覚えておいて、ログイン後に戻れるようにする
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  
  return children;
};

export default ProtectedRoute;

ProtectedRouteの仕組み

if (!user) {
  return <Navigate to="/login" state={{ from: location }} replace />;
}

ユーザーがログインしていない場合、ログインページにリダイレクトします。 state={{ from: location }}で、元々アクセスしようとしたページの情報を保存しています。

保護されたルートの設定

認証が必要なルートを設定しましょう:

// src/App.js
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { AuthProvider } from './contexts/AuthContext';
import ProtectedRoute from './components/ProtectedRoute';
import LoginPage from './pages/LoginPage';
import DashboardLayout from './pages/DashboardLayout';

const 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={<DashboardHome />} />
            <Route path="profile" element={<DashboardProfile />} />
            <Route path="settings" element={<DashboardSettings />} />
          </Route>
          
          <Route 
            path="/admin/*" 
            element={
              <ProtectedRoute>
                <AdminRoutes />
              </ProtectedRoute>
            }
          />
        </Routes>
      </BrowserRouter>
    </AuthProvider>
  );
};

認証が必要なルートをProtectedRouteでラップすることで、ログインしていないユーザーのアクセスを制限できます。

ログインページの実装

ログイン後に元のページに戻る機能を実装しましょう:

// src/pages/LoginPage.js
import { useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';

const LoginPage = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);
  
  const { login } = useAuth();
  const navigate = useNavigate();
  const location = useLocation();
  
  // ログイン前にアクセスしようとしたページ
  const from = location.state?.from?.pathname || '/dashboard';
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    setError('');
    setLoading(true);
    
    const result = await login(email, password);
    
    if (result.success) {
      navigate(from, { replace: true });
    } else {
      setError(result.error);
    }
    
    setLoading(false);
  };
  
  return (
    <div className="login-page">
      <div className="login-form-container">
        <h1>ログイン</h1>
        
        {error && (
          <div className="error-message">
            {error}
          </div>
        )}
        
        <form onSubmit={handleSubmit}>
          <div className="form-group">
            <label>メールアドレス</label>
            <input
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
            />
          </div>
          
          <div className="form-group">
            <label>パスワード</label>
            <input
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              required
            />
          </div>
          
          <button type="submit" disabled={loading}>
            {loading ? 'ログイン中...' : 'ログイン'}
          </button>
        </form>
      </div>
    </div>
  );
};

export default LoginPage;

ログイン後のリダイレクト

const from = location.state?.from?.pathname || '/dashboard';

// ログイン成功時
navigate(from, { replace: true });

ログイン後に、元々アクセスしようとしたページにリダイレクトします。 replace: trueで、ログインページを履歴から削除しています。

404 エラーページの実装

存在しないページへのアクセスを処理する方法です。

404 ページコンポーネント

ユーザーフレンドリーな404ページを作成しましょう:

// src/pages/NotFoundPage.js
import { Link } from 'react-router-dom';

const NotFoundPage = () => {
  return (
    <div className="not-found-page">
      <div className="not-found-content">
        <h1>404</h1>
        <h2>ページが見つかりません</h2>
        <p>お探しのページは存在しないか、移動された可能性があります。</p>
        
        <div className="not-found-actions">
          <Link to="/" className="btn btn-primary">
            ホームに戻る
          </Link>
          <button onClick={() => window.history.back()} className="btn btn-secondary">
            前のページに戻る
          </button>
        </div>
        
        <div className="helpful-links">
          <h3>こちらもご覧ください</h3>
          <ul>
            <li><Link to="/products">商品一覧</Link></li>
            <li><Link to="/about">会社について</Link></li>
            <li><Link to="/contact">お問い合わせ</Link></li>
          </ul>
        </div>
      </div>
    </div>
  );
};

export default NotFoundPage;

この404ページでは、ユーザーが次に取るべきアクションを明確に提示しています。 ホームページへのリンクや、他の主要ページへのリンクも用意しています。

キャッチオールルートの設定

すべての未定義ルートをキャッチする設定です:

// src/App.js
const App = () => {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<HomePage />} />
        <Route path="/about" element={<AboutPage />} />
        <Route path="/products" element={<ProductsPage />} />
        <Route path="/users/:id" element={<UserPage />} />
        
        {/* すべてのルートの最後に配置 */}
        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    </BrowserRouter>
  );
};

キャッチオールルートの重要性

<Route path="*" element={<NotFoundPage />} />

path="*"により、定義されていないすべてのルートをキャッチします。 これを最後に配置することで、存在しないURLにアクセスされた場合の処理を統一できます。

動的パンくずリストの実装

現在のページ位置を示すパンくずリストを実装します。

パンくずリストコンポーネント

URLパスから動的にパンくずリストを生成しましょう:

// src/components/Breadcrumbs.js
import { Link, useLocation } from 'react-router-dom';

const Breadcrumbs = () => {
  const location = useLocation();
  
  // URLパスを分割してパンくずを生成
  const pathnames = location.pathname.split('/').filter(x => x);
  
  // パンくずの表示名を定義
  const breadcrumbNames = {
    dashboard: 'ダッシュボード',
    users: 'ユーザー',
    products: '商品',
    settings: '設定',
    profile: 'プロフィール',
    admin: '管理',
    edit: '編集'
  };
  
  return (
    <nav className="breadcrumbs">
      <ol>
        <li>
          <Link to="/">ホーム</Link>
        </li>
        
        {pathnames.map((value, index) => {
          const to = `/${pathnames.slice(0, index + 1).join('/')}`;
          const isLast = index === pathnames.length - 1;
          const displayName = breadcrumbNames[value] || value;
          
          return (
            <li key={to}>
              <span className="separator">/</span>
              {isLast ? (
                <span className="current">{displayName}</span>
              ) : (
                <Link to={to}>{displayName}</Link>
              )}
            </li>
          );
        })}
      </ol>
    </nav>
  );
};

export default Breadcrumbs;

パンくずリストの生成ロジック

const pathnames = location.pathname.split('/').filter(x => x);

pathnames.map((value, index) => {
  const to = `/${pathnames.slice(0, index + 1).join('/')}`;
  // ...
});

現在のURLパスを分割して、各階層に対応するリンクを生成しています。 最後の要素は現在のページなので、リンクではなくテキストとして表示します。

高度なパンくずリスト

動的パラメータを解決して、より意味のあるパンくずリストを作成する方法です:

// src/components/SmartBreadcrumbs.js
import { Link, useLocation, useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';

const SmartBreadcrumbs = () => {
  const location = useLocation();
  const params = useParams();
  const [breadcrumbs, setBreadcrumbs] = useState([]);
  
  useEffect(() => {
    const generateBreadcrumbs = async () => {
      const pathnames = location.pathname.split('/').filter(x => x);
      const breadcrumbItems = [{ name: 'ホーム', path: '/' }];
      
      let currentPath = '';
      
      for (let i = 0; i < pathnames.length; i++) {
        const segment = pathnames[i];
        currentPath += `/${segment}`;
        
        // 動的パラメータの場合、実際のデータを取得
        if (segment === params.id) {
          try {
            const response = await fetch(`/api/users/${segment}`);
            const user = await response.json();
            breadcrumbItems.push({
              name: user.name,
              path: currentPath
            });
          } catch (error) {
            breadcrumbItems.push({
              name: segment,
              path: currentPath
            });
          }
        } else {
          // 静的セグメントの場合
          const displayNames = {
            dashboard: 'ダッシュボード',
            users: 'ユーザー',
            products: '商品',
            admin: '管理',
            edit: '編集'
          };
          
          breadcrumbItems.push({
            name: displayNames[segment] || segment,
            path: currentPath
          });
        }
      }
      
      setBreadcrumbs(breadcrumbItems);
    };
    
    generateBreadcrumbs();
  }, [location, params]);
  
  return (
    <nav className="breadcrumbs">
      <ol>
        {breadcrumbs.map((item, index) => {
          const isLast = index === breadcrumbs.length - 1;
          
          return (
            <li key={item.path}>
              {index > 0 && <span className="separator">/</span>}
              {isLast ? (
                <span className="current">{item.name}</span>
              ) : (
                <Link to={item.path}>{item.name}</Link>
              )}
            </li>
          );
        })}
      </ol>
    </nav>
  );
};

export default SmartBreadcrumbs;

動的パラメータの解決

if (segment === params.id) {
  try {
    const response = await fetch(`/api/users/${segment}`);
    const user = await response.json();
    breadcrumbItems.push({
      name: user.name,
      path: currentPath
    });
  } catch (error) {
    // エラー時は ID をそのまま表示
  }
}

URLパラメータ(例:ユーザーID)を実際の名前に変換することで、より意味のあるパンくずリストを作成できます。

まとめ

React Routerは、ReactアプリケーションでSPA(シングルページアプリケーション)を構築するための重要なライブラリです。

React Routerの主な機能

  • 基本的なルーティング: URLとコンポーネントの対応
  • 動的パラメータ: URLパラメータによる動的な画面表示
  • プログラムナビゲーション: JavaScriptによるページ遷移
  • ネストルート: 複雑な階層構造の管理

これらの機能により、従来のWebサイトと同じような直感的なナビゲーションを、Reactアプリケーションでも実現できます。

実践的な活用方法

  • 認証の実装: ログイン状態による画面制御
  • エラー処理: 404ページや存在しないルートの処理
  • ユーザー体験の向上: パンくずリストや直感的なナビゲーション
  • SEO対策: 適切なURL設計とページ構造

これらの実装により、ユーザーにとって使いやすいアプリケーションを作成できます。

開発のポイント

  • シンプルな設定から始める: 基本的なルーティングから段階的に拡張
  • 適切な粒度での設計: ネストルートを使って整理された構造を作る
  • ユーザビリティの考慮: 直感的なナビゲーションと明確なURL設計
  • エラーハンドリング: 存在しないページや認証エラーの適切な処理

最初は基本的な機能から始めて、アプリケーションの成長に合わせて機能を追加していくことをおすすめします。

注意点とベストプラクティス

  • React Router v6の使用: 最新バージョンの機能を活用
  • 適切なコンポーネント分割: 関心の分離を意識した設計
  • パフォーマンスの考慮: 必要に応じてレイジーローディングを実装
  • テストの実装: ルーティングのテストも忘れずに

React Routerを適切に活用することで、ユーザーフレンドリーで保守性の高いSPAを構築できます。

React Routerは、現代のWebアプリケーション開発において欠かせないツールです。 ぜひ、実際のプロジェクトで活用して、その利便性を体験してみてください!

関連記事