第13回
React Routerでページを繋ぐ:SPAのナビゲーションを実装する
React Router v7を使ってトップページと登場人物紹介ページを繋ぐ。LinkとaタグのSPAでの違い、ルート定義の仕組みをヘッダーのナビゲーション実装で学ぶ。
制作・運営:React Guide 運営者
SPAとルーティング
このプロジェクトはSPA(シングルページアプリケーション)。ブラウザには最初に1つのHTMLが読み込まれ、以降はJavaScriptがURLを書き換えながら表示するコンポーネントを切り替える。
<a href="/characters"> はブラウザのデフォルト動作(サーバーにHTTPリクエストを送ってページ全体を読み込み直す)が発生する。SPAとしての高速な画面切り替えが失われる。
React Routerの <Link> は通常の同一アプリ内遷移で、文書全体の再読み込みを避ける。データ取得や遅延ロードによるHTTP通信は発生しうる。共通レイアウトのstateは保持されるが、URLを読むHeaderなどは再レンダーされる。
React Routerをインストールする
npm install react-router-domルートを定義する
どのURLにどのコンポーネントを表示するかを定義する。
// src/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 にルート定義を置く。第12回の characters-page.tsx は CharactersPage.tsx にファイル名をそろえる。HomePage.tsx は、これまでのトップページのmain内を移し、export const HomePage = () => (...) として用意する。Header・FooterはAppに残す。
// 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 AppHeaderに<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">
React Guide
</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">
React Guide
</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>
)
}📁 第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">
React Guide
</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が生まれる瞬間。
確認してみる
/charactersへリンクで移動し、同じURLを新しいタブで直接開く。両方で人物ページが出ることを確かめる。
同じテーマの教材
自分のパソコンでReactを動かそう(つぎの一歩)
StackBlitzで作ったものを自分のパソコンでも動かす方法を小学生向けに解説。Node.jsの準備とViteでReactプロジェクトを作る手順、そして次に学ぶことを案内します。
じゃんけんゲームを作ろう(勝ち負けを判定)
コンピュータとじゃんけんできるゲームを作る回。自分の手をボタンで選び、相手の手を乱数で決めて、勝ち・負け・あいこを判定する方法を小学生向けに解説します。
4択クイズを作ろう(正解と点数を出そう)
選択肢を押すと正解・不正解がわかり点数がたまる4択クイズを作る回。配列とmapで選択肢を並べ、stateで点数を管理する方法を小学生向けにやさしく解説します。