ルーティング(React Router)
この章では、React RouterによるSPAのルーティングを学びます。
この章で学ぶこと
- React Routerの基本(Routes、Route、Link)
- 動的ルートとパラメータ取得
- ネストしたルートとOutlet
- 保護されたルートとリダイレクト
この章では03章で作成したプロジェクトを使用します。npm run devで開発サーバーを起動し、コードを書きながら学習を進めましょう。
React Routerとは
React Routerは、Reactアプリケーションでクライアントサイドルーティングを実現するライブラリです。
SPAとクライアントサイドルーティング
従来のWebサイトでは、ページ遷移のたびにサーバーからHTMLを取得していました。SPA(Single Page Application)では、最初に1つのHTMLを読み込み、以降のページ遷移はJavaScriptで処理します。
従来のWebサイト:
/home → サーバーリクエスト → home.html
/about → サーバーリクエスト → about.html
SPA:
/home → JavaScriptでコンポーネント切り替え
/about → JavaScriptでコンポーネント切り替え(リクエストなし)
React Routerは、URLの変更を検知してReactコンポーネントを切り替えることで、この仕組みを実現しています。
セットアップ
npm install react-router
本書ではReact Router v7(2024 年 11 月リリース、執筆時点 7.17.0)を使用しています。v7 ではパッケージが react-router に統合されたため、v6 以前のチュートリアルにある react-router-dom のインストールは不要です。
React Router v7 の 3 つのモード
React Router v7 は、用途に応じて3つのモードを使い分けられます。どのモードも同じ react-router パッケージから提供されるため、コードを段階的に移行できます。
| モード | 特徴 | 向いているケース |
|---|---|---|
| Declarative(宣言的) | <BrowserRouter> + <Routes> + <Route> を JSX で書く。v6 以前とほぼ同じ記法 | 小規模な SPA、学習目的、既存プロジェクトからの移行 |
| Data | createBrowserRouter + loader / action でデータ取得・更新をルーター側に寄せる | データ取得を伴う本格的な SPA(ほとんどの実務案件) |
| Framework | Vite プラグイン経由で SSR / SSG / ファイルベースルーティング / 型生成が有効に | Next.js の代替として、フルスタックアプリを構築したい場合 |
本章では Declarative モードで基本を学び、後半で Data モード(createBrowserRouter)を扱います。Framework モードは Next.js(25章)と同じ役割を担うため、SPA 主体の本書ではスコープ外とします。
新規の SPA では、データ取得を useEffect で書くよりも Data モードの loader でルーター側にデータ取得を宣言するほうが推奨です。ルーター側の API で初回表示時に並列フェッチが実現でき、ウォーターフォール問題を避けられます。
基本的な使い方
ルーターの設定
// main.tsx
import { BrowserRouter } from 'react-router'
createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<App />
</BrowserRouter>
)
BrowserRouter vs HashRouter
React Routerには複数のルーター種類があります。
| ルーター | URL形式 | 用途 |
|---|---|---|
BrowserRouter | /about | 一般的なWebアプリ(推奨) |
HashRouter | /#/about | サーバー設定ができない環境 |
MemoryRouter | なし | テスト用 |
BrowserRouterを使用する場合、サーバー側で全てのパスをindex.htmlに向ける設定が必要です(Viteの開発サーバーは自動で対応)。
ルートの定義
// App.tsx
import { Routes, Route } from 'react-router'
function App() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/users" element={<Users />} />
<Route path="*" element={<NotFound />} />
</Routes>
)
}
ナビゲーション
Linkコンポーネント
import { Link } from 'react-router'
function Navigation() {
return (
<nav>
<Link to="/">ホーム</Link>
<Link to="/about">About</Link>
<Link to="/users">ユーザー</Link>
</nav>
)
}
NavLink(アクティブ状態付き)
import { NavLink } from 'react-router'
function Navigation() {
return (
<nav>
<NavLink
to="/"
className={({ isActive }) =>
isActive ? 'text-blue-500 font-bold' : 'text-gray-600'
}
>
ホーム
</NavLink>
</nav>
)
}
プログラムによるナビゲーション
import { useNavigate } from 'react-router'
function LoginForm() {
const navigate = useNavigate()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
await login()
navigate('/dashboard') // ログイン後にダッシュボードへ
}
return <form onSubmit={handleSubmit}>...</form>
}
<a href="...">タグを直接使うと、ページ全体がリロードされSPAの利点が失われます。ページ内遷移には必ずLinkまたはuseNavigateを使用してください。
試してみよう: 基本ルーティング
Home、About、Contactの3ページを作成し、ナビゲーションで切り替えられるようにしてみましょう。
ヒント
RoutesとRouteでパスとコンポーネントを対応付けLinkでナビゲーションを作成path="*"でNotFoundページを追加
回答と解説
// src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router'
import App from './App'
import './index.css'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
)
// src/App.tsx
import { Routes, Route, Link } from 'react-router'
function Home() {
return <h1 className="text-2xl font-bold">ホームページ</h1>
}
function About() {
return <h1 className="text-2xl font-bold">Aboutページ</h1>
}
function Contact() {
return <h1 className="text-2xl font-bold">お問い合わせページ</h1>
}
function NotFound() {
return <h1 className="text-2xl font-bold text-red-500">404 - ページが見つかりません</h1>
}
function App() {
return (
<div className="p-8">
<nav className="mb-8 space-x-4">
<Link to="/" className="text-blue-500 hover:underline">ホーム</Link>
<Link to="/about" className="text-blue-500 hover:underline">About</Link>
<Link to="/contact" className="text-blue-500 hover:underline">お問い合わせ</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Routes>
</div>
)
}
export default App
BrowserRouterでアプリ全体をラップし、Routes内にRouteを定義します。Linkコンポーネントを使うことで、ページ全体をリロードせずにSPA内で遷移できます。path="*"は他のどのパスにもマッチしなかった場合のフォールバックです。
動的ルート
パラメータの取得
import { useParams } from 'react-router'
// ルート定義
<Route path="/users/:userId" element={<UserDetail />} />
// コンポーネント
function UserDetail() {
const { userId } = useParams<{ userId: string }>()
return <h1>ユーザーID: {userId}</h1>
}
クエリパラメータ
import { useSearchParams } from 'react-router'
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams()
const page = searchParams.get('page') || '1'
const sort = searchParams.get('sort') || 'newest'
const updatePage = (newPage: number) => {
setSearchParams({ page: String(newPage), sort })
}
const handleNextPage = () => {
updatePage(Number(page) + 1)
}
return (
<div>
<p>ページ: {page}, ソート: {sort}</p>
<button onClick={handleNextPage}>
次のページ
</button>
</div>
)
}
試してみよう: 動的ルート
/users/:userIdのルートを作成し、useParamsでユーザーIDを取得して表示してみましょう。
ヒント
- ルート定義で
:userIdのように動的セグメントを指定 useParamsでその値を取得- 型引数
<{ userId: string }>で型安全に
回答と解説
// src/App.tsx
import { Routes, Route, Link, useParams } from 'react-router'
function UserList() {
const users = [
{ id: 1, name: '田中太郎' },
{ id: 2, name: '山田花子' },
{ id: 3, name: '鈴木一郎' }
]
return (
<div>
<h1 className="text-2xl font-bold mb-4">ユーザー一覧</h1>
<ul className="space-y-2">
{users.map(user => (
<li key={user.id}>
<Link
to={`/users/${user.id}`}
className="text-blue-500 hover:underline"
>
{user.name}
</Link>
</li>
))}
</ul>
</div>
)
}
function UserDetail() {
const { userId } = useParams<{ userId: string }>()
// 実際のアプリではAPIからユーザー情報を取得
const userNames: Record<string, string> = {
'1': '田中太郎',
'2': '山田花子',
'3': '鈴木一郎'
}
const userName = userId ? userNames[userId] : '不明'
return (
<div>
<h1 className="text-2xl font-bold mb-4">ユーザー詳細</h1>
<p className="mb-2">ユーザーID: {userId}</p>
<p className="mb-4">名前: {userName}</p>
<Link to="/users" className="text-blue-500 hover:underline">
← 一覧に戻る
</Link>
</div>
)
}
function App() {
return (
<div className="p-8">
<nav className="mb-8">
<Link to="/" className="text-blue-500 hover:underline mr-4">ホーム</Link>
<Link to="/users" className="text-blue-500 hover:underline">ユーザー</Link>
</nav>
<Routes>
<Route path="/" element={<h1 className="text-2xl font-bold">ホーム</h1>} />
<Route path="/users" element={<UserList />} />
<Route path="/users/:userId" element={<UserDetail />} />
</Routes>
</div>
)
}
export default App
:userIdのようにコロンで始まるセグメントは動的パラメータです。useParamsフックで取得した値は常に文字列型なので、数値として使用する場合はNumber(userId)で変換します。
ネストしたルート
// App.tsx
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="users" element={<UsersLayout />}>
<Route index element={<UserList />} />
<Route path=":userId" element={<UserDetail />} />
</Route>
</Route>
</Routes>
)
}
// Layout.tsx
import { Outlet } from 'react-router'
function Layout() {
return (
<div>
<Header />
<main>
<Outlet /> {/* 子ルートがここにレンダリングされる */}
</main>
<Footer />
</div>
)
}
試してみよう: ネストしたルート
共通レイアウト(Header + Footer)を持つLayoutコンポーネントを作成し、Outletで子ルートをレンダリングしてみましょう。
ヒント
- 親ルートで
<Layout />をelementに設定 - 子ルートのコンポーネントは
<Outlet />の位置にレンダリング NavLinkのendプロパティで完全一致のときだけアクティブに
回答と解説
// src/components/Layout.tsx
import { Outlet, NavLink } from 'react-router'
export function Layout() {
const linkClass = ({ isActive }: { isActive: boolean }) =>
`px-4 py-2 rounded ${isActive ? 'bg-blue-500 text-white' : 'text-gray-600 hover:bg-gray-100'}`
return (
<div className="min-h-screen flex flex-col">
<header className="bg-white shadow">
<nav className="max-w-4xl mx-auto px-4 py-4 flex gap-2">
<NavLink to="/" className={linkClass} end>
ホーム
</NavLink>
<NavLink to="/about" className={linkClass}>
About
</NavLink>
<NavLink to="/contact" className={linkClass}>
お問い合わせ
</NavLink>
</nav>
</header>
<main className="flex-1 max-w-4xl mx-auto px-4 py-8 w-full">
<Outlet />
</main>
<footer className="bg-gray-100 py-4 text-center text-gray-600">
© {new Date().getFullYear()} My App
</footer>
</div>
)
}
// src/pages/Home.tsx
export function Home() {
return (
<div>
<h1 className="text-3xl font-bold mb-4">ホームページへようこそ</h1>
<p className="text-gray-600">
これはReact Routerを使用したSPAのサンプルです。
</p>
</div>
)
}
// src/pages/About.tsx
export function About() {
return (
<div>
<h1 className="text-3xl font-bold mb-4">About</h1>
<p className="text-gray-600">
このアプリケーションについての説明ページです。
</p>
</div>
)
}
// src/pages/Contact.tsx
export function Contact() {
return (
<div>
<h1 className="text-3xl font-bold mb-4">お問い合わせ</h1>
<p className="text-gray-600">
お問い合わせはこちらからお願いします。
</p>
</div>
)
}
// src/App.tsx
import { Routes, Route } from 'react-router'
import { Layout } from './components/Layout'
import { Home } from './pages/Home'
import { About } from './pages/About'
import { Contact } from './pages/Contact'
function NotFound() {
return (
<div>
<h1 className="text-3xl font-bold text-red-500 mb-4">404</h1>
<p>ページが見つかりませんでした。</p>
</div>
)
}
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="contact" element={<Contact />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
)
}
export default App
ネストしたルートでは、親ルートの<Outlet />の位置に子ルートのコンポーネントがレンダリングされます。NavLinkのendプロパティは、完全一致のときだけアクティブになるようにするためのものです(/が/aboutなどでもアクティブにならないように)。共通レイアウトを1箇所にまとめることで、コードの重複を避けられます。
保護されたルート
import { Navigate, useLocation } from 'react-router'
type ProtectedRouteProps = {
children: React.ReactNode
}
function ProtectedRoute({ children }: ProtectedRouteProps) {
const { isAuthenticated } = useAuth()
const location = useLocation()
if (!isAuthenticated) {
// ログインページにリダイレクト(元のURLを保存)
return <Navigate to="/login" state={{ from: location }} replace />
}
return <>{children}</>
}
// 使用例
<Route
path="/dashboard"
element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
}
/>
ログイン後のリダイレクト
function LoginPage() {
const navigate = useNavigate()
const location = useLocation()
const from = location.state?.from?.pathname || '/'
const handleLogin = async () => {
await login()
navigate(from, { replace: true }) // 元のページに戻る
}
return <button onClick={handleLogin}>ログイン</button>
}
データローダー(推奨パターン)
Data モード(createBrowserRouter)では、ルートにローダーを設定できます(v6.4 で導入され、v7 では Data モードの中核機能になっています)。
データローダーは、コンポーネントがレンダリングされる前にデータを取得するパターンです。TanStack Query(20章)と組み合わせての使用も可能です。
import { createBrowserRouter, useLoaderData } from 'react-router'
import type { LoaderFunctionArgs } from 'react-router'
// RouterProvider だけは 'react-router/dom' から import する(react-dom 連携版)
import { RouterProvider } from 'react-router/dom'
// ローダー関数
async function userLoader({ params }: LoaderFunctionArgs) {
const response = await fetch(`/api/users/${params.userId}`)
if (!response.ok) throw new Error('User not found')
return response.json()
}
// ルーター設定
const router = createBrowserRouter([
{
path: '/users/:userId',
element: <UserDetail />,
loader: userLoader,
errorElement: <ErrorPage />
}
])
// コンポーネント
function UserDetail() {
const user = useLoaderData() as User
return <h1>{user.name}</h1>
}
// main.tsx
createRoot(document.getElementById('root')!).render(
<RouterProvider router={router} />
)
ローディング状態
useNavigation は Data モード(createBrowserRouter + RouterProvider)専用のフックです。前半の <BrowserRouter> 構成では使えません。
import { useNavigation } from 'react-router'
function App() {
const navigation = useNavigation()
return (
<div>
{navigation.state === 'loading' && <LoadingBar />}
<Outlet />
</div>
)
}
エラーハンドリング
import { useRouteError, isRouteErrorResponse } from 'react-router'
function ErrorPage() {
const error = useRouteError()
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status}</h1>
<p>{error.statusText}</p>
</div>
)
}
return (
<div>
<h1>エラーが発生しました</h1>
<p>{error instanceof Error ? error.message : '不明なエラー'}</p>
</div>
)
}
ページタイトルの管理(React 19 の Document Metadata)
SPA では URL が変わってもブラウザタブのタイトルが変わらないため、ルートごとに document.title を更新する必要があります。React 19 では、JSX で <title> / <meta> / <link> を書くだけで自動的に <head> へ巻き上げられるため、これが簡潔になりました。
// ページコンポーネントの中で直接書くだけでOK
function AboutPage() {
return (
<>
<title>会社について | MyApp</title>
<meta name="description" content="MyAppの会社情報ページです" />
<h1>会社について</h1>
<p>...</p>
</>
)
}
React 19 より前は react-helmet-async などのライブラリを使っていましたが、新規の SPA では React 19 内蔵の機能だけで十分です。
SSR(Next.js)での注意: Next.js には専用の metadata API(25章で解説)があり、React 19 の Document Metadata と機能が重複します。そのため Next.js 側では metadata / generateMetadata を使うのが基本です。Next.js のドキュメントでも「ページ単位のメタデータは metadata export を使う」と案内されています。
まとめ
- React Routerはクライアントサイドルーティングのデファクトスタンダード
RoutesとRouteでルート定義Link/NavLinkでナビゲーションuseParamsでURLパラメータ取得useSearchParamsでクエリパラメータ管理Outletでネストしたルートをレンダリング- 保護されたルートで認証制御
- ローダーでデータ取得を宣言的に
- React 19 では
<title>/<meta>を JSX で直接書くだけで<head>に反映される
次の章では、パフォーマンス最適化について学びます。
次に読む
発展的トピック へ進みます。パフォーマンス最適化、デバッグ、クラスコンポーネント、Next.js 入門など発展的トピックを扱います。