第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 AppHeader.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が生まれる瞬間。