第13回

React Routerでページを繋ぐ:SPAのナビゲーションを実装する

React Router v7を使ってトップページと登場人物紹介ページを繋ぐ。LinkとaタグのSPAでの違い、ルート定義の仕組みをヘッダーのナビゲーション実装で学ぶ。

·18分で読める
たける
たける ヘッダーの「登場人物」リンクを `<a href="/characters">` で書いたら動くんですけど、クリックのたびにページ全体がリロードされてる気がします。
りこ
りこ リロードされてる。それがSPAと `<a>` タグの問題。React Routerの `<Link>` を使うとリロードなしで切り替わる。

SPAとルーティング

このプロジェクトはSPA(シングルページアプリケーション)。ブラウザには最初に1つのHTMLが読み込まれ、以降はJavaScriptがURLを書き換えながら表示するコンポーネントを切り替える。

<a href="/characters"> はブラウザのデフォルト動作(サーバーにHTTPリクエストを送ってページ全体を読み込み直す)が発生する。SPAとしての高速な画面切り替えが失われる。

React Routerの <Link> はURLを変えるがHTTPリクエストは発生させない。Reactがコンポーネントを入れ替えるだけで、ヘッダーやフッターは再描画されない。

React Routerをインストールする

npm install react-router-dom

ルートを定義する

どのURLにどのコンポーネントを表示するかを定義する。

// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import App from './App.tsx'
import './index.css'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<App />} />
      </Routes>
    </BrowserRouter>
  </StrictMode>,
)

ページが増えてきたら App.tsx でレイアウトと子ルートを管理する構造にする。

// src/App.tsx
import { Routes, Route } from 'react-router-dom'
import { Header } from './components/Header'
import { Footer } from './components/Footer'
import { HomePage } from './pages/HomePage'
import { CharactersPage } from './pages/CharactersPage'

function App() {
  return (
    <div>
      <Header />
      <main>
        <Routes>
          <Route path="/" element={<HomePage />} />
          <Route path="/characters" element={<CharactersPage />} />
        </Routes>
      </main>
      <Footer />
    </div>
  )
}

export default App
たける
たける `<Header />` と `<Footer />` が `<Routes>` の外にあるから、ページが変わってもヘッダーとフッターは残るってことですね。
りこ
りこ そう。`<Routes>` の中だけが切り替わる。それがSPAのレイアウト設計の基本。

Headerに<Link>を使う

// src/components/Header.tsx
import { Link } from 'react-router-dom'

export const Header = () => {
  return (
    <header className="bg-white border-b border-slate-200 sticky top-0 z-10">
      <div className="max-w-5xl mx-auto px-4 sm:px-8 h-14 flex items-center justify-between">
        <Link to="/" className="font-bold text-slate-900">
          生成AI時代のReact実践入門
        </Link>
        <nav className="flex items-center gap-6">
          <Link to="/" className="text-sm text-slate-600 hover:text-slate-900">
            ホーム
          </Link>
          <Link to="/characters" className="text-sm text-slate-600 hover:text-slate-900">
            登場人物
          </Link>
        </nav>
      </div>
    </header>
  )
}

href の代わりに to を使う。それだけの違いで、ページのリロードなしに遷移できる。

現在のページをアクティブ表示する

useLocation フックで現在のURLを取得し、ナビゲーションのアクティブ状態を表示できる。

import { Link, useLocation } from 'react-router-dom'

export const Header = () => {
  const { pathname } = useLocation()

  const navItems = [
    { to: '/',           label: 'ホーム' },
    { to: '/characters', label: '登場人物' },
  ]

  return (
    <header className="bg-white border-b border-slate-200 sticky top-0 z-10">
      <div className="max-w-5xl mx-auto px-4 sm:px-8 h-14 flex items-center justify-between">
        <Link to="/" className="font-bold text-slate-900">
          生成AI時代のReact実践入門
        </Link>
        <nav className="flex items-center gap-6">
          {navItems.map(item => (
            <Link
              key={item.to}
              to={item.to}
              className={`text-sm transition-colors ${
                pathname === item.to
                  ? 'text-sky-600 font-semibold'
                  : 'text-slate-600 hover:text-slate-900'
              }`}
            >
              {item.label}
            </Link>
          ))}
        </nav>
      </div>
    </header>
  )
}
たける
たける `pathname === item.to` で今いるページのリンクだけ色が変わる。これ、すごく実用的ですね。
りこ
りこ ほぼ全てのサイトにある機能。stateを使わなくても、URLから現在地が取れるのがReact Routerの便利なところ。
ユナ
ユナ バックエンド視点で補足すると、SPAは「すべてのURLに対してindex.htmlを返す」ようにサーバーを設定しないといけない。Nginxなら `try_files $uri /index.html` 、Cloudflare Pagesなら `_redirects` ファイルがそれをやっている。
たける
たける あ、だから次の回でデプロイするときに `_redirects` を作るんですね。サーバー側の設定とセットなんだ。
ユナ
ユナ そう。React Router自体はブラウザ上で動くので、ブラウザがURLを解釈する前にサーバーが正しく `index.html` を返してくれないと404になる。フロントとサーバーの両方への理解が必要な部分。
📁 第13回完了時点のファイル構成・完成コード

フォルダ構成

src/
├── components/
│   ├── Header.tsx      ← <Link> を使うように更新
│   └── Footer.tsx
├── pages/
│   ├── HomePage.tsx
│   └── CharactersPage.tsx
├── App.tsx             ← Routes を定義
└── main.tsx            ← BrowserRouter を追加

main.tsx

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App.tsx'
import './index.css'

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

App.tsx

import { Routes, Route } from 'react-router-dom'
import { Header } from './components/Header'
import { Footer } from './components/Footer'
import { HomePage } from './pages/HomePage'
import { CharactersPage } from './pages/CharactersPage'

function App() {
  return (
    <div className="min-h-screen flex flex-col">
      <Header />
      <main className="flex-1">
        <Routes>
          <Route path="/" element={<HomePage />} />
          <Route path="/characters" element={<CharactersPage />} />
        </Routes>
      </main>
      <Footer />
    </div>
  )
}

export default App

Header.tsx(Link + useLocation 版)

import { Link, useLocation } from 'react-router-dom'

export const Header = () => {
  const { pathname } = useLocation()

  const navItems = [
    { to: '/',           label: 'ホーム' },
    { to: '/characters', label: '登場人物' },
  ]

  return (
    <header className="bg-white border-b border-slate-200 sticky top-0 z-10">
      <div className="max-w-5xl mx-auto px-4 sm:px-8 h-14 flex items-center justify-between">
        <Link to="/" className="font-bold text-slate-900">
          生成AI時代のReact実践入門
        </Link>
        <nav className="flex items-center gap-6">
          {navItems.map(item => (
            <Link
              key={item.to}
              to={item.to}
              className={`text-sm transition-colors ${
                pathname === item.to
                  ? 'text-sky-600 font-semibold'
                  : 'text-slate-600 hover:text-slate-900'
              }`}
            >
              {item.label}
            </Link>
          ))}
        </nav>
      </div>
    </header>
  )
}

まとめ

  • SPAでは <a> ではなく <Link to="..."> を使う。<a> はページ全体をリロードする
  • <Routes> の外に配置したコンポーネント(Header・Footer)はページ切り替えでも再描画されない
  • useLocation() で現在のURLパスが取れる。ナビゲーションのアクティブ状態に活用できる

次の最終回、第14回ではビルドしてCloudflare Pagesに公開する。URLが生まれる瞬間。