はじめに

前回の記事では、fetchやaxiosを使ったAPI連携とデータ取得について解説しました。本記事では、複数ページを持つSPA(シングルページアプリケーション)を構築するための「React Router」について詳しく解説します。

従来のWebサイトでは、ページ遷移のたびにブラウザがサーバーへリクエストを送り、新しいHTMLを取得していました。しかし、SPAではJavaScriptがクライアント側でページの表示を切り替えるため、高速でシームレスなユーザー体験を提供できます。React Routerは、このSPAのルーティング機能を実現するための標準的なライブラリです。

本記事を読むことで、以下のことができるようになります。

  • React Routerのセットアップと基本設定
  • Routes・Routeコンポーネントによるルーティング定義
  • 動的ルートとuseParamsによるパラメータ取得
  • ネストしたルートとOutletによるレイアウト構成
  • Link・NavLinkによる宣言的ナビゲーション
  • useNavigateによるプログラマティックな画面遷移

実行環境・前提条件

必要な環境

  • Node.js 20.x以上
  • Viteで作成したReactプロジェクト(TypeScript推奨)
  • VS Code(推奨)

前提知識

  • 関数コンポーネントの基本
  • useStateとuseEffectの使い方
  • Propsによるデータの受け渡し

SPAとは何か - 従来のWebサイトとの違い

従来のマルチページアプリケーション(MPA)

従来のWebサイトでは、ページ遷移のたびに以下の処理が行われます。

  1. ユーザーがリンクをクリック
  2. ブラウザがサーバーへHTTPリクエストを送信
  3. サーバーが新しいHTMLを生成して返却
  4. ブラウザがHTMLを受け取り、ページ全体を再描画

この方式では、ページ遷移のたびに画面が白くなり、ユーザー体験が断片的になります。

シングルページアプリケーション(SPA)

SPAでは、最初に1つのHTMLファイルを読み込み、その後はJavaScriptがページの表示を切り替えます。

  1. 初回アクセス時にHTML・CSS・JavaScriptを読み込み
  2. ユーザーがリンクをクリック
  3. JavaScriptがURLを更新し、対応するコンポーネントを表示
  4. 必要に応じてAPIからデータを取得

この方式により、高速でスムーズなページ遷移が実現できます。React Routerは、このURLとコンポーネントの対応関係(ルーティング)を管理するライブラリです。

React Routerのセットアップ

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

まず、React Routerをプロジェクトにインストールします。2024年末にリリースされたReact Router v7では、パッケージ名がreact-routerに統一されました。

1
npm install react-router

BrowserRouterによるアプリケーションのラップ

React Routerを使用するには、アプリケーション全体をBrowserRouterコンポーネントでラップします。main.tsx(またはエントリーポイント)を以下のように修正してください。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router';
import App from './App';
import './index.css';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

BrowserRouterは、HTML5のHistory APIを使用してURLを管理します。これにより、ブラウザの戻る・進むボタンが正しく動作し、クリーンなURLが実現できます。

基本的なルーティングの設定

RoutesとRouteコンポーネント

ルーティングは、RoutesRouteコンポーネントを使って定義します。Routesはルート定義のコンテナで、Routeは個々のURLパターンとコンポーネントの対応を表します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { Routes, Route } from 'react-router';

// ページコンポーネント
function Home() {
  return <h1>ホームページ</h1>;
}

function About() {
  return <h1>このサイトについて</h1>;
}

function Contact() {
  return <h1>お問い合わせ</h1>;
}

// メインのAppコンポーネント
function App() {
  return (
    <div>
      <h1>My App</h1>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
      </Routes>
    </div>
  );
}

export default App;

上記の設定により、以下のルーティングが有効になります。

URL 表示されるコンポーネント
/ <Home />
/about <About />
/contact <Contact />

indexルート

index属性を使用すると、親ルートのURLでデフォルトとして表示される子ルートを定義できます。

1
2
3
4
5
6
7
<Routes>
  <Route path="/" element={<Layout />}>
    {/* / にアクセスした時に表示される */}
    <Route index element={<Home />} />
    <Route path="about" element={<About />} />
  </Route>
</Routes>

Linkによるナビゲーション

Linkコンポーネントの基本

ページ間を移動するには、HTMLの<a>タグではなく、React RouterのLinkコンポーネントを使用します。LinkはSPA内でのナビゲーションを行い、ページ全体の再読み込みを防ぎます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import { Link } from 'react-router';

function Navigation() {
  return (
    <nav>
      <ul>
        <li><Link to="/">ホーム</Link></li>
        <li><Link to="/about">About</Link></li>
        <li><Link to="/contact">お問い合わせ</Link></li>
      </ul>
    </nav>
  );
}

Linkとaタグの違い

<a>タグと<Link>コンポーネントの違いを理解しておきましょう。

特性 <a> タグ <Link> コンポーネント
ページ遷移 ページ全体を再読み込み クライアントサイドルーティング
速度 遅い(サーバーリクエスト) 高速(JavaScript処理)
状態の保持 失われる 保持される
用途 外部サイトへのリンク SPA内のページ遷移

現在のページに対応するナビゲーションリンクにスタイルを適用したい場合は、NavLinkコンポーネントを使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { NavLink } from 'react-router';

function Navigation() {
  return (
    <nav>
      <NavLink 
        to="/" 
        className={({ isActive }) => isActive ? 'active' : ''}
      >
        ホーム
      </NavLink>
      <NavLink 
        to="/about" 
        className={({ isActive }) => isActive ? 'active' : ''}
      >
        About
      </NavLink>
      <NavLink 
        to="/contact"
        style={({ isActive }) => ({
          fontWeight: isActive ? 'bold' : 'normal',
          color: isActive ? 'blue' : 'black'
        })}
      >
        お問い合わせ
      </NavLink>
    </nav>
  );
}

NavLinkは自動的に現在のURLと比較し、一致する場合はisActivetrueになります。classNamestyleに関数を渡すことで、アクティブ状態に応じたスタイリングが可能です。

また、NavLinkがアクティブな場合は自動的に.activeクラスが付与されるため、CSSで以下のようにスタイルを定義することもできます。

1
2
3
4
a.active {
  color: blue;
  font-weight: bold;
}

動的ルートとuseParams

動的セグメントとは

実際のアプリケーションでは、/users/1/users/2のように、URLの一部が動的に変化するケースが多くあります。React Routerでは、パスの一部を:から始めることで「動的セグメント」として定義できます。

1
<Route path="/users/:userId" element={<UserDetail />} />

この設定により、/users/1/users/abcなど、/users/以降の任意の文字列にマッチするようになります。

useParamsでパラメータを取得

動的セグメントの値は、useParamsフックで取得できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { useParams } from 'react-router';

interface UserParams {
  userId: string;
}

function UserDetail() {
  // URLパラメータを取得
  const { userId } = useParams<UserParams>();

  return (
    <div>
      <h1>ユーザー詳細</h1>
      <p>ユーザーID: {userId}</p>
    </div>
  );
}

複数の動的パラメータ

1つのルートに複数の動的セグメントを含めることも可能です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// ルート定義
<Route path="/categories/:categoryId/products/:productId" element={<ProductDetail />} />

// コンポーネント
function ProductDetail() {
  const { categoryId, productId } = useParams();
  
  return (
    <div>
      <p>カテゴリID: {categoryId}</p>
      <p>商品ID: {productId}</p>
    </div>
  );
}

実践例: ブログ記事詳細ページ

動的ルートを使った実践的な例として、ブログ記事の詳細ページを実装してみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import { Routes, Route, Link, useParams } from 'react-router';

// 記事データ(実際はAPIから取得)
const posts = [
  { id: '1', title: 'React入門', content: 'Reactの基本を解説します。' },
  { id: '2', title: 'TypeScript入門', content: 'TypeScriptの基本を解説します。' },
  { id: '3', title: 'React Router入門', content: 'ルーティングの基本を解説します。' },
];

// 記事一覧コンポーネント
function PostList() {
  return (
    <div>
      <h1>記事一覧</h1>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <Link to={`/posts/${post.id}`}>{post.title}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

// 記事詳細コンポーネント
function PostDetail() {
  const { postId } = useParams<{ postId: string }>();
  const post = posts.find(p => p.id === postId);

  if (!post) {
    return <p>記事が見つかりませんでした。</p>;
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      <Link to="/posts">一覧に戻る</Link>
    </article>
  );
}

// Appコンポーネント
function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/posts" element={<PostList />} />
      <Route path="/posts/:postId" element={<PostDetail />} />
    </Routes>
  );
}

ネストしたルートとOutlet

ネストしたルートとは

複数のページで共通のレイアウト(ヘッダー、サイドバーなど)を使用したい場合、ネストしたルートが便利です。親ルートでレイアウトを定義し、子ルートで各ページのコンテンツを表示します。

1
2
3
4
5
6
7
<Routes>
  <Route path="/dashboard" element={<DashboardLayout />}>
    <Route index element={<DashboardHome />} />
    <Route path="settings" element={<Settings />} />
    <Route path="profile" element={<Profile />} />
  </Route>
</Routes>

この設定により、以下のURLが有効になります。

URL 表示されるコンポーネント
/dashboard DashboardLayout + DashboardHome
/dashboard/settings DashboardLayout + Settings
/dashboard/profile DashboardLayout + Profile

Outletコンポーネント

親ルートのコンポーネントでは、Outletコンポーネントを使って子ルートのコンテンツを表示する場所を指定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { Outlet, Link } from 'react-router';

function DashboardLayout() {
  return (
    <div className="dashboard">
      <header>
        <h1>ダッシュボード</h1>
        <nav>
          <Link to="/dashboard">ホーム</Link>
          <Link to="/dashboard/settings">設定</Link>
          <Link to="/dashboard/profile">プロフィール</Link>
        </nav>
      </header>
      
      <main>
        {/* 子ルートのコンポーネントがここに表示される */}
        <Outlet />
      </main>
      
      <footer>
        <p>Dashboard Footer</p>
      </footer>
    </div>
  );
}

function DashboardHome() {
  return <h2>ダッシュボードへようこそ</h2>;
}

function Settings() {
  return <h2>設定ページ</h2>;
}

function Profile() {
  return <h2>プロフィールページ</h2>;
}

レイアウトルート(pathなしのルート)

path属性を省略したルートは「レイアウトルート」として機能し、URLに影響を与えずに共通レイアウトを適用できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<Routes>
  {/* レイアウトルート(URLには影響しない) */}
  <Route element={<MarketingLayout />}>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
    <Route path="/contact" element={<Contact />} />
  </Route>
  
  {/* 別のレイアウトを適用 */}
  <Route element={<AdminLayout />}>
    <Route path="/admin" element={<AdminHome />} />
    <Route path="/admin/users" element={<AdminUsers />} />
  </Route>
</Routes>

この設定では、//about/contactはすべてMarketingLayout内に表示され、/admin/admin/usersAdminLayout内に表示されます。

useNavigateによるプログラマティックな遷移

useNavigateとは

ユーザーのクリックではなく、プログラムの処理結果に応じてページ遷移を行いたい場合があります。例えば、フォーム送信後のリダイレクトや、ログイン成功後のダッシュボードへの遷移などです。このような場合はuseNavigateフックを使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { useNavigate } from 'react-router';

function LoginForm() {
  const navigate = useNavigate();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    
    // ログイン処理(仮)
    const success = await performLogin();
    
    if (success) {
      // ダッシュボードへ遷移
      navigate('/dashboard');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* フォームの内容 */}
      <button type="submit">ログイン</button>
    </form>
  );
}

navigate関数には、いくつかのオプションを指定できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 基本的な遷移
navigate('/dashboard');

// 履歴を置き換える(戻るボタンで戻れなくする)
navigate('/dashboard', { replace: true });

// 履歴を前後に移動
navigate(-1);  // 1つ前のページへ(戻るボタンと同じ)
navigate(1);   // 1つ次のページへ(進むボタンと同じ)
navigate(-2);  // 2つ前のページへ

// stateを渡す
navigate('/checkout', { state: { from: 'cart' } });

実践例: フォーム送信後のリダイレクト

商品をカートに追加した後、カートページへリダイレクトする例を見てみましょう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { useState } from 'react';
import { useNavigate } from 'react-router';

interface Product {
  id: string;
  name: string;
  price: number;
}

function ProductPage({ product }: { product: Product }) {
  const navigate = useNavigate();
  const [isAdding, setIsAdding] = useState(false);

  const handleAddToCart = async () => {
    setIsAdding(true);
    
    try {
      // カートに追加する処理(仮)
      await addToCart(product.id);
      
      // カートページへ遷移
      navigate('/cart', { 
        state: { message: `${product.name}をカートに追加しました` }
      });
    } catch (error) {
      console.error('カートへの追加に失敗しました', error);
    } finally {
      setIsAdding(false);
    }
  };

  return (
    <div>
      <h1>{product.name}</h1>
      <p>価格: {product.price}</p>
      <button onClick={handleAddToCart} disabled={isAdding}>
        {isAdding ? '追加中...' : 'カートに追加'}
      </button>
    </div>
  );
}

URLSearchParamsの活用

useSearchParamsフック

URLのクエリパラメータ(?key=value)を扱うには、useSearchParamsフックを使用します。検索機能やフィルタリング機能の実装に便利です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { useSearchParams } from 'react-router';

function SearchResults() {
  const [searchParams, setSearchParams] = useSearchParams();
  
  // クエリパラメータを取得
  const query = searchParams.get('q') || '';
  const category = searchParams.get('category') || 'all';

  const handleCategoryChange = (newCategory: string) => {
    // クエリパラメータを更新
    setSearchParams({ q: query, category: newCategory });
  };

  return (
    <div>
      <p>検索キーワード: {query}</p>
      <p>カテゴリ: {category}</p>
      
      <select 
        value={category} 
        onChange={(e) => handleCategoryChange(e.target.value)}
      >
        <option value="all">すべて</option>
        <option value="electronics">電子機器</option>
        <option value="books">書籍</option>
      </select>
    </div>
  );
}

404ページの実装

キャッチオールルート

存在しないURLにアクセスされた場合に表示する404ページは、*(スプラット)を使って実装します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
function NotFound() {
  return (
    <div>
      <h1>404 - ページが見つかりません</h1>
      <p>お探しのページは存在しないか、移動した可能性があります。</p>
      <Link to="/">ホームに戻る</Link>
    </div>
  );
}

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      {/* 他のすべてのURLにマッチ */}
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

path="*"は、他のどのルートにもマッチしなかったURLをすべてキャッチします。必ずルート定義の最後に配置してください。

完成サンプル: ブログアプリケーション

ここまで学んだ内容を組み合わせた、完成形のサンプルコードを見てみましょう。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import { 
  Routes, 
  Route, 
  Link, 
  NavLink, 
  Outlet, 
  useParams, 
  useNavigate 
} from 'react-router';

// データ(実際はAPIから取得)
const posts = [
  { id: '1', title: 'React入門', content: 'Reactの基本を解説します。', author: 'Alice' },
  { id: '2', title: 'TypeScript入門', content: 'TypeScriptの基本を解説します。', author: 'Bob' },
  { id: '3', title: 'React Router入門', content: 'ルーティングの基本を解説します。', author: 'Alice' },
];

// レイアウトコンポーネント
function Layout() {
  return (
    <div className="app">
      <header>
        <h1>My Blog</h1>
        <nav>
          <NavLink to="/" end className={({ isActive }) => isActive ? 'active' : ''}>
            ホーム
          </NavLink>
          <NavLink to="/posts" className={({ isActive }) => isActive ? 'active' : ''}>
            記事一覧
          </NavLink>
          <NavLink to="/about" className={({ isActive }) => isActive ? 'active' : ''}>
            About
          </NavLink>
        </nav>
      </header>
      
      <main>
        <Outlet />
      </main>
      
      <footer>
        <p>2025 My Blog. All rights reserved.</p>
      </footer>
    </div>
  );
}

// ホームページ
function Home() {
  return (
    <div>
      <h2>ようこそ</h2>
      <p>このブログではReactに関する情報を発信しています</p>
      <Link to="/posts">記事を読む</Link>
    </div>
  );
}

// 記事一覧
function PostList() {
  return (
    <div>
      <h2>記事一覧</h2>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <Link to={`/posts/${post.id}`}>
              {post.title} - by {post.author}
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

// 記事詳細
function PostDetail() {
  const { postId } = useParams<{ postId: string }>();
  const navigate = useNavigate();
  const post = posts.find(p => p.id === postId);

  if (!post) {
    return (
      <div>
        <h2>記事が見つかりません</h2>
        <button onClick={() => navigate('/posts')}>一覧に戻る</button>
      </div>
    );
  }

  return (
    <article>
      <h2>{post.title}</h2>
      <p className="author">著者: {post.author}</p>
      <div className="content">{post.content}</div>
      <button onClick={() => navigate(-1)}>戻る</button>
    </article>
  );
}

// Aboutページ
function About() {
  return (
    <div>
      <h2>このサイトについて</h2>
      <p>React学習のためのブログサイトです</p>
    </div>
  );
}

// 404ページ
function NotFound() {
  return (
    <div>
      <h2>404 - ページが見つかりません</h2>
      <Link to="/">ホームに戻る</Link>
    </div>
  );
}

// メインApp
function App() {
  return (
    <Routes>
      <Route element={<Layout />}>
        <Route path="/" element={<Home />} />
        <Route path="/posts" element={<PostList />} />
        <Route path="/posts/:postId" element={<PostDetail />} />
        <Route path="/about" element={<About />} />
        <Route path="*" element={<NotFound />} />
      </Route>
    </Routes>
  );
}

export default App;

このサンプルでは、以下の機能を実装しています。

  • 共通レイアウト(ヘッダー、フッター)
  • NavLinkによるアクティブ状態の表示
  • 動的ルートによる記事詳細ページ
  • useNavigateによる戻るボタンの実装
  • 404ページのハンドリング

よくあるエラーと解決策

BrowserRouterが見つからないエラー

1
Error: useNavigate() may be used only in the context of a <Router> component.

このエラーは、useNavigateLinkなどのReact Routerのフックやコンポーネントが、BrowserRouterの外で使用された場合に発生します。

解決策として、main.tsxでアプリケーション全体をBrowserRouterでラップしてください。

本番環境での404エラー

SPAをNetlifyやVercelにデプロイした際、直接URLにアクセスすると404エラーになることがあります。これは、サーバーがそのパスのHTMLファイルを探そうとするためです。

解決策として、すべてのリクエストをindex.htmlにリダイレクトする設定が必要です。

Netlifyの場合、public/_redirectsファイルを作成します。

1
/*    /index.html   200

Vercelの場合、vercel.jsonを作成します。

1
2
3
4
5
{
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
}

まとめ

本記事では、React Routerを使ったSPAのルーティングについて解説しました。

  • BrowserRouterでアプリケーションをラップし、ルーティングを有効化
  • RoutesRouteでURLとコンポーネントの対応を定義
  • LinkNavLinkで宣言的なナビゲーションを実装
  • useParamsで動的ルートのパラメータを取得
  • Outletでネストしたルートのレイアウトを構成
  • useNavigateでプログラマティックな画面遷移を実現

React Routerをマスターすることで、複数ページを持つ本格的なWebアプリケーションを構築できるようになります。次の記事では、アプリケーション全体で状態を共有するための「グローバル状態管理」について解説します。

次に読む記事

参考リンク