これまでは1つのページだけを表示するコンポーネントを作ってきました。
しかし、実際のWebアプリでは「ホームページ」「プロフィールページ」「お問い合わせページ」など、複数のページを行き来できることが必要です。
そのために使うのが React Router です。
Reactアプリにページ遷移機能を追加するためのライブラリで、現在最も広く使われています。
この記事では、React Routerの基本的な使い方から、実践的なパターンまでステップごとに学んでいきます。
Contents
React Routerとは?
「そもそもルーティングって何?」というところから丁寧に解説します。
React Routerがどんなライブラリでどんなことができるのか、全体像をつかんだうえでインストールまで進めましょう。
このセクションを読めば、React Routerを使う理由と基本の考え方が理解できます。
ルーティングってなに?
「ルーティング」とは、URLに応じて表示する画面を切り替える仕組みのことです。
たとえば、
https://example.com/→ ホーム画面を表示https://example.com/about→ 概要画面を表示https://example.com/contact→ お問い合わせ画面を表示
というように、URLが変わるたびに対応するコンポーネントを表示します。
React Routerの主な特徴
- ページ全体をリロードしないページ遷移(SPA:シングルページアプリケーション)
- ブラウザの「戻る」「進む」ボタンへの対応
- URLから値を取り出す(例:
/users/123の123部分) - ログインしていないと見られないページの制御
インストール
npm install react-router-domインストール後、以下のような警告が表示されることがあります。
3 vulnerabilities (1 moderate, 2 high)
To address all issues, run:
npm audit fix
Run `npm audit` for details.この警告は何?
vulnerabilities(脆弱性)とは、依存パッケージに潜むセキュリティ上の問題のことです。
React Router本体ではなく、その内部で使われているパッケージに起因することがほとんどです。
対処方法
まず npm audit fix を試す
npm audit fixこれで自動的に安全なバージョンへ更新されます。多くの場合はこれで解消します。
それでも残る場合は様子を見てOK
npm audit fix を実行しても警告が消えないことがあります。
これは「修正バージョンがまだリリースされていない」か「メジャーバージョンアップが必要で自動修正できない」ケースです。
学習・個人開発の段階では、残った警告をそのまま放置しても問題ありません。
本番リリースが近づいたタイミングで改めて確認するようにしましょう。
基本的なルーティング
React Routerを使った、もっともシンプルな構成を学びます。
「URLが変わると別のコンポーネントが表示される」という基本の流れを実際のコードで確認しましょう。
このセクションを読めば、複数ページを切り替える最小構成を自分で書けるようになります。
シンプルなルート設定
まずは一番シンプルな例として、3つのページを切り替えるアプリを作ります。
JavaScript版:
// App.jsx
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'
// 各ページのコンポーネント(今は仮の内容)
function Home() {
return <h1>ホームページ</h1>
}
function About() {
return <h1>概要ページ</h1>
}
function Contact() {
return <h1>お問い合わせページ</h1>
}
function App() {
return (
// BrowserRouter:アプリ全体をルーティング機能で包む
<BrowserRouter>
{/* ナビゲーションメニュー */}
<nav>
<Link to="/">ホーム</Link>
<Link to="/about">概要</Link>
<Link to="/contact">お問い合わせ</Link>
</nav>
{/* URLに応じてどのページを表示するか設定する */}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</BrowserRouter>
)
}
export default AppTypeScript版:
// App.tsx
import { type JSX } from 'react'
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'
function Home(): JSX.Element {
return <h1>ホームページ</h1>
}
function About(): JSX.Element {
return <h1>概要ページ</h1>
}
function Contact(): JSX.Element {
return <h1>お問い合わせページ</h1>
}
function App(): JSX.Element {
return (
<BrowserRouter>
<nav>
<Link to="/">ホーム</Link>
<Link to="/about">概要</Link>
<Link to="/contact">お問い合わせ</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
</Routes>
</BrowserRouter>
)
}
export default App主要コンポーネントの解説
| コンポーネント | 役割 |
|---|---|
BrowserRouter | アプリ全体をラップして、ルーティング機能を有効にする |
Routes | 複数の Route をまとめる。URLに一致した最初の1つだけを表示する |
Route | URLのパス(path)と表示するコンポーネント(element)を対応付ける |
Link | ページ遷移のためのリンク。<a> タグと違い、ページ全体をリロードしない |
ざっくりと説明すると
「Routesの内側にあるRouteコンポーネントのpathプロパティに一致するurlにアクセスしたら、elementプロパティで指定されたコンポーネントが表示される」
💡 なぜ <a> タグではなく <Link> を使うの?
<a> タグはクリックするとページ全体を読み込み直します。<Link> を使うと必要な部分だけ更新されるため、表示が速く、スムーズな操作感を実現できます。
レイアウトコンポーネント
ヘッダーやフッターのように「すべてのページで共通して表示したいUI」を、ページごとにコピー&ペーストするのは大変です。
このセクションでは、共通部分を一か所にまとめる「レイアウトコンポーネント」パターンを学びます。
このセクションを読めば、コードの重複をなくして、保守しやすいアプリ構成が作れるようになります。
共通レイアウトの作成
JavaScript版:
// components/Layout.jsx
import { Outlet, Link } from 'react-router-dom'
function Layout() {
return (
<div>
{/* すべてのページに表示されるヘッダー */}
<header>
<nav>
<Link to="/">ホーム</Link>
<Link to="/about">概要</Link>
<Link to="/contact">お問い合わせ</Link>
</nav>
</header>
<main>
{/* ここに各ページのコンポーネントが差し込まれる */}
<Outlet />
</main>
{/* すべてのページに表示されるフッター */}
<footer>
<p>© 2026 マイサイト</p>
</footer>
</div>
)
}
export default Layout💡 <Outlet /> とは?
親ルートの中で「子ページをここに表示してください」と指定する場所です。
たとえば /about にアクセスすると、<Outlet /> の位置に <About /> コンポーネントが差し込まれます。
App.jsxでの使用:
このセクションからコンポーネント(Home,About,Contact )はpagesフォルダに切り出しました。
挙動を確認したい場合は、作成してみてください。
コードは省略しますが、ここのでの理解があれば、Appコンポーネント内に書いていた各ページのコンポーネントは別のファイルに切り出せるはずです。
// App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Layout from './components/Layout'
import Home from './pages/Home'
import About from './pages/About'
import Contact from './pages/Contact'
function App() {
return (
<BrowserRouter>
<Routes>
{/* Layout を親ルートにすることで、すべての子ページにヘッダー・フッターが適用される */}
<Route path="/" element={<Layout />}>
<Route index element={<Home />} /> {/* / にアクセスしたとき */}
<Route path="about" element={<About />} /> {/* /about にアクセスしたとき */}
<Route path="contact" element={<Contact />} />{/* /contact にアクセスしたとき */}
</Route>
</Routes>
</BrowserRouter>
)
}
export default Appすべての子ページのLayout を親ルート(Routeで囲む)にすることに着目してください。
HomeのRouteにはindexを入れてます。デフォルトのページとして認識されます。
<Route path=”/” element={<Home />} />とすると?
子ルートの path は相対パスとして親のパスに連結されます。
なので path="/" と書くと、/ から始まる絶対パスとして解釈され、親のパスを無視して単独で / にマッチしようとします。
動作はしますが、親を無視した絶対パス指定になってしまい、意図しない挙動になる可能性があります。
index は親とパスが競合して混乱の原因を避けて、そのあたりの複雑さを意識せずに「親と同じパスのデフォルトページ」と明示できる専用の書き方なので、React Router公式でも推奨されています。
TypeScript版:
// components/Layout.tsx
import { type JSX } from 'react'
import { Outlet, Link } from 'react-router-dom'
function Layout(): JSX.Element {
return (
<div>
<header>
<nav>
<Link to="/">ホーム</Link>
<Link to="/about">概要</Link>
<Link to="/contact">お問い合わせ</Link>
</nav>
</header>
<main>
<Outlet />
</main>
<footer>
<p>© 2025 マイサイト</p>
</footer>
</div>
)
}
export default LayoutURLパラメータ
/users/123 や /blog/react/456 のように、URLの一部に可変の値を含む「動的なURL」を扱う方法を学びます。
このセクションを読めば、ユーザーIDや記事IDなどをURLから取り出して、対応するデータを表示できるようになります。
動的ルート
JavaScript版:
// pages/UserProfile.jsx
import { useParams } from 'react-router-dom'
function UserProfile() {
const { userId } = useParams()
return (
<div>
<h1>ユーザープロフィール</h1>
<p>ユーザーID: {userId}</p>
</div>
)
}
export default UserProfileApp.jsxでの設定:
<Routes>
<Route path="/users/:userId" element={<UserProfile />} />
</Routes>
// 使用例
<Link to="/users/123">ユーザー123</Link>
<Link to="/users/456">ユーザー456</Link>TypeScript版:
// pages/UserProfile.tsx
import { type JSX } from 'react'
import { useParams } from 'react-router-dom'
interface Params extends Record<string, string | undefined> {
userId: string
}
function UserProfile(): JSX.Element {
const { userId } = useParams<Params>()
return (
<div>
<h1>ユーザープロフィール</h1>
<p>ユーザーID: {userId}</p>
</div>
)
}
export default UserProfile
useParams が返す値は、URLにそのパラメータが存在しない場合 undefined になる可能性があります。
なので型の制約が string | undefined になっています。
複数のパラメータ
JavaScript版:
// pages/BlogPost.jsx
import { useParams } from 'react-router-dom'
function BlogPost() {
const { category, postId } = useParams()
return (
<div>
<h1>ブログ記事</h1>
<p>カテゴリ: {category}</p>
<p>記事ID: {postId}</p>
</div>
)
}
export default BlogPost// ルート設定
<Route path="/blog/:category/:postId" element={<BlogPost />} />
// 使用例
<Link to="/blog/react/123">React記事123</Link>TypeScript版:
// pages/BlogPost.tsx
import { type JSX } from 'react'
import { useParams } from 'react-router-dom'
function BlogPost(): JSX.Element {
const { category, postId } = useParams<{ category: string; postId: string }>()
return (
<div>
<h1>ブログ記事</h1>
<p>カテゴリ: {category}</p>
<p>記事ID: {postId}</p>
</div>
)
}
export default BlogPost💡 ; と , どっちが正しいの?
6行目の以下の部分に注目してください。
{ category: string; postId: string }TypeScriptのオブジェクト型の中ではどちらも正しく、同じ意味です。
慣習的にオブジェクトの「値」には ”,”、オブジェクトの「型」には” ;“ を使うことが多いです。
TypeScriptの公式ドキュメントでも型定義には ; が使われています。
💡 string | undefinedとしなくてもエラーないの?
useParams<{ category: string; postId: string }>()TypeScriptの仕様で、interface と インライン型 {} の扱いが異なります。
インライン型はインデックスシグネチャを暗黙的にあるとみなします。
// インライン型 → 暗黙的にインデックスシグネチャを持つとみなされる → エラーなし
useParams<{ category: string; postId: string }>()
// interface → インデックスシグネチャを明示しないと持たないとみなされる → エラー
interface Params {
userId: string // インデックスシグネチャがないと判定される
}
useParams<Params>() // ❌ エラーinterface は「後から拡張できる(declaration merging)」という性質を持つため、TypeScriptはインデックスシグネチャを勝手に付けることができません。
どんなプロパティが後から追加されるか保証できないからです。
一方のインライン型はその場で完結しているので、TypeScriptが安全に暗黙のインデックスシグネチャを付けられます。
クエリパラメータ
/search?q=react&page=2 のように、URLの ? 以降に付く値(クエリパラメータ)を読み書きする方法を学びます。
検索キーワードやページ番号のように、URLに状態を持たせたいときに活躍します。
このセクションを読めば、検索結果ページやページネーションをURLベースで実装できるようになります。
useSearchParams
useSearchParams は、URLの ? 以降のクエリパラメータを読み書きするためのフックです。
Reactの useState に似た構造で、現在の値を取得する関数とURLを更新する関数をセットで返します。
const [searchParams, setSearchParams] = useSearchParams()
// ↑ 読み取り用 ↑ 書き込み用読み取り:searchParams.get('キー名')
// URL が /search?q=react&page=2 のとき
searchParams.get('q') // → 'react'
searchParams.get('page') // → '2'
searchParams.get('sort') // → null(存在しないキーはnullになる)書き込み:setSearchParams({ キー: 値 })
// ボタンを押すと URL が /search?q=react&page=2 に変わる
setSearchParams({ q: 'react', page: '2' })💡 useState との違い
useState はコンポーネントの中だけに状態を持ちますが、useSearchParams はURLに状態を持ちます。
URLに状態があると
- 「ページをリロードしても検索条件が保持される」
- 「検索結果のURLをそのまま共有できる」
といったメリットがあります。
サンプル
JavaScript版:
import { useSearchParams } from 'react-router-dom'
function SearchResults() {
const [searchParams, setSearchParams] = useSearchParams()
const query = searchParams.get('q') || ''
const page = searchParams.get('page') || '1'
const handleSearch = (e) => {
e.preventDefault()
const form = e.currentTarget
const input = form.elements.namedItem('query')
setSearchParams({ q: input.value, page: '1' })
}
return (
<div>
<h1>検索結果</h1>
{/* handleSearch を実際に呼び出す検索フォーム */}
<form onSubmit={handleSearch}>
<input name="query" defaultValue={query} placeholder="キーワードを入力" />
<button type="submit">検索</button>
</form>
<p>検索キーワード: {query}</p>
<p>ページ: {page}</p>
<button onClick={() => setSearchParams({ q: query, page: '2' })}>
次のページ
</button>
</div>
)
}
export default SearchResults
// URL例: /search?q=react&page=1// ルート設定
<Route path="search" element={<SearchResults />} />
// 使用例
<Link to="/search?q=react&page=1">検索</Link>TypeScript版:
import { type JSX } from 'react'
import { useSearchParams } from 'react-router-dom'
function SearchResults(): JSX.Element {
const [searchParams, setSearchParams] = useSearchParams()
const query = searchParams.get('q') || ''
const page = searchParams.get('page') || '1'
const handleSearch = (e: React.FormEvent<HTMLFormElement>): void => {
e.preventDefault()
const form = e.currentTarget
const input = form.elements.namedItem('query') as HTMLInputElement
setSearchParams({ q: input.value, page: '1' })
}
return (
<div>
<h1>検索結果</h1>
{/* handleSearch を実際に呼び出す検索フォーム */}
<form onSubmit={handleSearch}>
<input
name="query"
defaultValue={query}
placeholder="キーワードを入力"
/>
<button type="submit">検索</button>
</form>
<p>検索キーワード: {query}</p>
<p>ページ: {page}</p>
<button onClick={() => setSearchParams({ q: query, page: '2' })}>
次のページ
</button>
</div>
)
}
export default SearchResultsプログラムによるナビゲーション
リンクをクリックする以外にも、「ログイン成功後に自動でダッシュボードへ移動する」など、コードの中からページ遷移を制御したい場面があります。
このセクションでは useNavigate フックを使った方法を学びます。
このセクションを読めば、ボタン操作や処理の結果に応じて任意のページへ移動させられるようになります。
useNavigate は、コードの中からページ遷移を実行するためのフックです。
返ってくる navigate 関数を呼び出すだけで、任意のページへ移動できます。
const navigate = useNavigate()指定したパスへ移動する
navigate('/dashboard') // /dashboard へ移動
navigate('/users/123') // /users/123 へ移動履歴を残さずに移動する(replace)
navigate('/dashboard', { replace: true })通常の移動はブラウザの履歴に積まれるため「戻る」ボタンで前のページに戻れます。
replace: true を指定すると履歴を上書きするため戻れなくなります。
ログイン後のリダイレクトなど「ログイン画面に戻ってほしくない」場面で使います。
履歴を使って移動する
navigate(-1) // 1つ前のページへ(ブラウザの「戻る」と同じ)
navigate(1) // 1つ次のページへ(ブラウザの「進む」と同じ)
navigate(-2) // 2つ前のページへ💡 Link との使い分け
クリックで移動するだけなら <Link> で十分です。
「ログイン成功後」「フォーム送信後」など、処理の結果としてページを移動したいときに useNavigate を使いましょう。
使用例
JavaScript版:
import { useNavigate } from 'react-router-dom'
function LoginForm() {
const navigate = useNavigate()
const handleLogin = (e) => {
e.preventDefault()
// ログイン処理
const success = true
if (success) {
// ログイン成功後、ダッシュボードに遷移
navigate('/dashboard')
// または、履歴を置き換える(戻るボタンで戻れない)
// navigate('/dashboard', { replace: true })
}
}
const handleCancel = () => {
// 前のページに戻る
navigate(-1)
}
return (
<form onSubmit={handleLogin}>
<input type="email" placeholder="メール" />
<input type="password" placeholder="パスワード" />
<button type="submit">ログイン</button>
<button type="button" onClick={handleCancel}>キャンセル</button>
</form>
)
}
export default LoginForm遷移先であるduchboadページは作成していなかったので、作成するか遷移先を変更してください。
TypeScript版:
import { type JSX } from 'react'
import { useNavigate } from 'react-router-dom'
function LoginForm(): JSX.Element {
const navigate = useNavigate()
const handleLogin = (e: React.SubmitEvent<HTMLFormElement>): void => {
e.preventDefault()
const success = true
if (success) {
navigate('/about')
}
}
const handleCancel = (): void => {
navigate(-1)
}
return (
<form onSubmit={handleLogin}>
<input type="email" placeholder="メール" />
<input type="password" placeholder="パスワード" />
<button type="submit">ログイン</button>
<button type="button" onClick={handleCancel}>
キャンセル
</button>
</form>
)
}
export default LoginForm404ページ(Not Found)
存在しないURLにアクセスされたとき、何も表示されないままでは利用者が困惑します。
「ページが見つかりません」と伝える404ページの作り方を学びましょう。
このセクションを読めば、どんなURLにアクセスされても適切にエラーページを表示できるようになります。
404ページとは?
「404」とはWebの世界で「リソースが見つからない」を意味するHTTPステータスコードです。
たとえば「ユーザーがURLを打ち間違えたり」、「削除されたページにアクセスしたりしたとき」に返されます。
React Routerでは path="*" を使って「どのルートにも一致しなかった場合」を受け取ることができます。
/ → Home にマッチ ✅
/about → About にマッチ ✅
/abcxyz → どこにもマッチしない → path="*" が受け取る💡 path="*" は必ず一番最後に書く
Routes は上から順にURLを照合し、最初にマッチしたルートだけを表示します。
path="*" を先に書くとすべてのURLがここにマッチしてしまうため、必ず一番下に置きましょう。
JavaScript版:
// pages/NotFound.jsx
import { Link } from 'react-router-dom'
function NotFound() {
return (
<div>
<h1>404 - ページが見つかりません</h1>
<p>お探しのページは存在しません。</p>
<Link to="/">ホームに戻る</Link>
</div>
)
}
export default NotFoundApp.jsxでの設定:
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
{/* その他のルート */}
{/* !!!!”必ず最後”に404ルートを配置 */}
<Route path="*" element={<NotFound />} />
</Routes>TypeScript版:
// pages/NotFound.tsx
import { type JSX } from 'react'
import { Link } from 'react-router-dom'
function NotFound(): JSX.Element {
return (
<div>
<h1>404 - ページが見つかりません</h1>
<p>お探しのページは存在しません。</p>
<Link to="/">ホームに戻る</Link>
</div>
)
}
export default NotFoundナビゲーションメニューで「今どのページにいるか」を視覚的にわかりやすく表示したいことがあります。
NavLink を使うと、現在表示中のページに対応するリンクに自動でスタイルを適用できます。
このセクションを読めば、現在地がひと目でわかるナビゲーションメニューを実装できるようになります。
JavaScript版:
// /components/Navigation.tsx
import { NavLink } from 'react-router-dom'
function Navigation() {
return (
<nav>
<NavLink
to="/"
className={({ isActive }) => isActive ? 'active' : ''}
>
ホーム
</NavLink>
<NavLink
to="/about"
className={({ isActive }) => isActive ? 'active' : ''}
>
概要
</NavLink>
<NavLink
to="/contact"
style={({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
color: isActive ? 'red' : 'black'
})}
>
お問い合わせ
</NavLink>
</nav>
)
}
export default NavigationLayoutコンポーネント内のheader要素内に実装してください。
・・・省略
import Navigation from './Navigation'
function Layout(): JSX.Element {
return (
<div>
<header>
<Navigation />
・・・省略CSS:
/* 今回はApp.cssにもで追加してください。*/
.active {
font-weight: bold;
color: blue;
text-decoration: underline;
}TypeScript版:
// /components/Navigation.tsx
import { type JSX } from 'react'
import { NavLink } from 'react-router-dom'
function Navigation(): JSX.Element {
return (
<nav>
<NavLink to="/" className={({ isActive }) => (isActive ? 'active' : '')}>
ホーム
</NavLink>
<NavLink
to="/about"
className={({ isActive }) => (isActive ? 'active' : '')}
>
概要
</NavLink>
<NavLink
to="/contact"
style={({ isActive }) => ({
fontWeight: isActive ? 'bold' : 'normal',
color: isActive ? 'red' : 'black',
})}
>
お問い合わせ
</NavLink>
</nav>
)
}
export default Navigationどちらもページ遷移のためのコンポーネントですが、用途が異なります。
Link | NavLink | |
|---|---|---|
| ページ遷移 | ✅ | ✅ |
| 現在のURLと一致したか検知 | ❌ | ✅ |
| アクティブ時のスタイル適用 | ❌ | ✅ |
// Link:シンプルなリンク。アクティブ状態は関知しない
<Link to="/about">概要</Link>
// NavLink:現在のURLと一致したとき isActive が true になる
<NavLink to="/about">概要</NavLink>使い分けの目安
- ナビゲーションメニューのリンク →
NavLink(現在地を強調したいため) - 記事内のリンク、ボタン代わりのリンクなど →
Link(アクティブ状態が不要なため)
ネストされたルート
ダッシュボードのような画面では、「ダッシュボードの中にプロフィール・設定・メッセージなどのサブページがある」という入れ子構造が必要です。
このセクションでは、ルートを入れ子にして階層的なページ構成を実現する方法を学びます。
このセクションを読めば、複雑な画面構成でも整理されたルート設定が書けるようになります。
複雑なルート構造
JavaScript版:
// pages/Dashboard.jsx
import { Outlet, Link } from 'react-router-dom'
function Dashboard() {
return (
<div>
<h1>ダッシュボード</h1>
<nav>
<Link to="profile">プロフィール</Link>
<Link to="settings">設定</Link>
<Link to="messages">メッセージ</Link>
</nav>
<Outlet /> {/* サブページがここに表示される */}
</div>
)
}
export default Dashboard// pages/DashboardProfile.jsx
function DashboardProfile() {
return <h2>プロフィール</h2>
}
export default DashboardProfile
// pages/DashboardSettings.jsx
function DashboardSettings() {
return <h2>設定</h2>
}
export default DashboardSettings// pages/DashboardMessages.jsx
function DashboardMessages() {
return <h2>メッセージ</h2>
}
export default DashboardMessages// App.jsx
<Routes>
<Route path="/dashboard" element={<Dashboard />}>
<Route path="profile" element={<DashboardProfile />} />
<Route path="settings" element={<DashboardSettings />} />
<Route path="messages" element={<DashboardMessages />} />
</Route>
</Routes>
//Layout.jsx
// URL例:
// /dashboard/profile
// /dashboard/settings
// /dashboard/messagesTypeScript版:
// pages/Dashboard.tsx
import { type JSX } from 'react'
import { Outlet, Link } from 'react-router-dom'
function Dashboard(): JSX.Element {
return (
<div>
<h1>ダッシュボード</h1>
<nav>
<Link to="profile">プロフィール</Link>
<Link to="settings">設定</Link>
<Link to="messages">メッセージ</Link>
</nav>
<Outlet />
</div>
)
}
export default Dashboardネストされたルートの着眼点
❶ URLの構造とコンポーネントの構造が対応している
ネストされたルートを理解するうえで大切なのは、「URLの構造とコンポーネントの構造が対応している」という点です。
URL: /dashboard/profile
↑ ↑
親ルート 子ルート
コンポーネント: <Dashboard> の中に <DashboardProfile> が表示される💡 どんなときに使うか
画面の一部だけが切り替わるUIに向いています。たとえばダッシュボードでは、ヘッダーやサイドメニューは共通で、メインエリアだけがプロフィール・設定・メッセージに切り替わります。
❷レイアウトコンポーネントとの違い
「サイト全体で共通のヘッダー・フッター」ならレイアウトコンポーネント、「特定のページ内でさらにサブページに分かれる」ならネストされたルートと使い分けましょう。
<Routes>
{/* サイト全体の共通レイアウト */}
<Route path="/" element={<Layout />}>
{/* ダッシュボード内のネストされたルート */}
<Route path="dashboard" element={<Dashboard />}>
<Route path="profile" element={<DashboardProfile />} />
<Route path="settings" element={<DashboardSettings />} />
</Route>
・・・
・・・
<Route path="*" element={<NotFound />} />
</Route>
</Routes>保護されたルート(認証)
ログインしていないユーザーがダッシュボードなどの非公開ページに直接アクセスしようとした場合、ログインページにリダイレクトする仕組みが必要です。
このセクションでは「保護されたルート」パターンを学びます。
このセクションを読めば、認証状態に応じてページへのアクセスを制御できるようになります。
認証チェックを含むルート
JavaScript版:
// components/ProtectedRoute.jsx
import { Navigate } from 'react-router-dom'
function ProtectedRoute({ children, isAuthenticated }) {
if (!isAuthenticated) {
// ログインしていなければログインページにリダイレクト
return <Navigate to="/login" replace />
}
return children
}
export default ProtectedRoute// pages/Login.jsx
import { useNavigate } from 'react-router-dom'
function Login({ setAuth }) {
const navigate = useNavigate()
const handleLogin = (e) => {
e.preventDefault()
// 本来はここでAPIにIDとパスワードを送って認証する
// ここでは簡略化のため、ボタンを押したらログイン成功とみなす
setAuth(true)
navigate('/dashboard')
}
return (
<form onSubmit={handleLogin}>
<input type="email" placeholder="メール" />
<input type="password" placeholder="パスワード" />
<button type="submit">ログイン</button>
</form>
)
}
export default Login使用例:
// App.jsx
import { useState } from 'react'
import './App.css'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Home from './pages/Home'
import Dashboard from './pages/Dashboard'
import Profile from './pages/DashboardProfile'
import Login from './pages/Login'
import NotFound from './pages/NotFound'
import ProtectedRoute from './components/ProtectedRoute'
function App() {
const [isAuthenticated, setIsAuthenticated] = useState(false)
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login setAuth={setIsAuthenticated} />} />
{/* 保護されたルート */}
<Route
path="/dashboard"
element={
<ProtectedRoute isAuthenticated={isAuthenticated}>
<Dashboard />
</ProtectedRoute>
}
/>
<Route
path="/profile"
element={
<ProtectedRoute isAuthenticated={isAuthenticated}>
<Profile />
</ProtectedRoute>
}
/>
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
)
}TypeScript版:
// components/ProtectedRoute.tsx
import { Navigate } from 'react-router-dom'
interface ProtectedRouteProps {
children: React.ReactNode
isAuthenticated: boolean
}
function ProtectedRoute({ children, isAuthenticated }: ProtectedRouteProps): JSX.Element {
if (!isAuthenticated) {
return <Navigate to="/login" replace />
}
return <>{children}</>
}
export default ProtectedRoute// pages/Login.tsx
import { type JSX } from 'react'
import { useNavigate } from 'react-router-dom'
interface LoginProps {
setAuth: (value: boolean) => void
}
function Login({ setAuth }: LoginProps): JSX.Element {
const navigate = useNavigate()
const handleLogin = (e: React.SubmitEvent<HTMLFormElement>): void => {
e.preventDefault()
// 本来はここでAPIにIDとパスワードを送って認証する
// ここでは簡略化のため、ボタンを押したらログイン成功とみなす
setAuth(true)
navigate('/dashboard')
}
return (
<form onSubmit={handleLogin}>
<input type="email" placeholder="メール" />
<input type="password" placeholder="パスワード" />
<button type="submit">ログイン</button>
</form>
)
}
export default Login
着眼点
保護されたルートのポイントは「認証チェックをコンポーネントとして切り出す」ことです。
ユーザーが /dashboard にアクセス
↓
ProtectedRoute が認証状態を確認
↓
ログイン済み?
┌──── Yes ────┐
│ │
↓ ↓
<Dashboard /> <Navigate to="/login" />
を表示 にリダイレクトProtectedRoute を作っておくと、保護したいページを増やすときに認証ロジックをコピーせず、単純に ProtectedRoute で包むだけで済みます。
// 保護するページを増やすのが簡単
<Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
<Route path="/profile" element={<ProtectedRoute><Profile /></ProtectedRoute>} />
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />また <Navigate to="/login" replace /> の replace は、ブラウザの履歴を上書きするオプションです。
これがないと「ダッシュボード→ログインページ」の履歴が残り、ログイン後に「戻る」ボタンを押すとダッシュボードに戻ろうとして再びリダイレクトされるループが発生します。
セクション6で学んだ useNavigate と、ProtectedRoute で使っている <Navigate /> はどちらも「ページ遷移」のためのものですが、使う場面が異なります。
useNavigate | <Navigate /> | |
|---|---|---|
| 種類 | フック(Hook) | コンポーネント |
| 使う場面 | イベント処理の中で移動したいとき | レンダリング中に移動したいとき |
| 使い方 | navigate('/path') を呼び出す | JSXとして返す |
// useNavigate:ボタンを押したときなど「処理の中」で使う
function Login() {
const navigate = useNavigate()
const handleLogin = () => {
navigate('/dashboard') // 処理の結果として移動
}
return <button onClick={handleLogin}>ログイン</button>
}// Navigate:条件によって表示するコンポーネントを切り替える「JSXの中」で使う
function ProtectedRoute({ children, isAuthenticated }) {
if (!isAuthenticated) {
return <Navigate to="/login" replace /> // コンポーネントとして返す
}
return children
}ProtectedRoute の中では「認証されていなければリダイレクト先を表示する」という考え方のため、処理の中で navigate() を呼ぶのではなくコンポーネントとして <Navigate /> を返すのが自然な書き方です。
データローダー
loader 機能を使うと、ページが表示される前にデータをあらかじめ取得しておけます。(React Router v6.4から追加された )
従来の useEffect によるデータ取得より、シンプルかつ効率的に書けます。
このセクションを読めば、ページ表示と同時にAPIからデータを読み込む、より洗練されたコードが書けるようになります。
loaderを使ったデータ取得
JavaScript版:
//pages/UserDetail.jsx
import { useLoaderData } from 'react-router-dom'
// ①ローダー関数 export を付けて外から使えるようにする
export async function userLoader({ params }) {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${params.userId}`)
return response.json()
}
// コンポーネント
export default function UserDetail() {
const user = useLoaderData()
return (
<div>
<h1>{user.name}</h1>
<p>メール: {user.email}</p>
<p>電話: {user.phone}</p>
</div>
)
}//App.jsx
import { createBrowserRouter, RouterProvider} from 'react-router-dom'
import UserDetail, { userLoader } from './pages/UserDetail'
// ②ルーター設定
const router = createBrowserRouter([
{
path: '/',
element: <Home />
},
{
path: '/users/:userId',
element: <UserDetail />,
loader: userLoader
}
])
// ③Appコンンポーネント
export default function App() {
・・・・・省略
return <RouterProvider router={router} />
}TypeScript版:
//pages/UserDetail.tsx
import { type JSX } from 'react'
import { useLoaderData, LoaderFunctionArgs} from 'react-router-dom'
// ①ローダー関数 export を付けて外から使えるようにする
export async function userLoader({ params }: LoaderFunctionArgs): Promise<User> {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${params.userId}`)
return response.json()
}
interface User {
id: number
name: string
email: string
phone: string
}
//コンポーネント
export default function UserDetail(): JSX.Element {
const user = useLoaderData() as User
return (
<div>
<h1>{user.name}</h1>
<p>メール: {user.email}</p>
<p>電話: {user.phone}</p>
</div>
)
}
//App.tsx
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import UserDetail, { userLoader } from './pages/UserDetail'
interface User {
id: number
name: string
email: string
phone: string
}
// ②ルーター設定
const router = createBrowserRouter([
{
path: '/',
element: <Home />
},
{
path: '/users/:userId',
element: <UserDetail />,
loader: userLoader
}
])
// ③Appコンンポーネント
export default function App(): JSX.Element {
return <RouterProvider router={router} />
}着眼点
これまでのデータ取得は useEffect を使って「コンポーネントが表示されてから取得する」という流れでした。
// これまでの書き方(useEffect)
function UserDetail() {
const [user, setUser] = useState(null)
// ① コンポーネントが表示される
// ② useEffect が走ってデータ取得開始
// ③ データが取れたら再レンダリング
useEffect(() => {
fetch(`/users/${userId}`).then(res => res.json()).then(setUser)
}, [userId])
if (!user) return <p>読み込み中...</p> // 一瞬ローディング表示が挟まる
return <h1>{user.name}</h1>
}loader を使うと「ページに移動する前にデータを取得する」という流れに変わります。
useEffect の場合: ページ表示 → データ取得 → 再レンダリング(ローディングが挟まる)
loader の場合: データ取得 → ページ表示(最初からデータがある)また BrowserRouter ではなく createBrowserRouter を使う点も重要です。
// createBrowserRouter:ルートをオブジェクトの配列で定義する場合
const router = createBrowserRouter([
{
path: '/users/:userId',
element: <UserDetail />,
loader: userLoader, // このルートに来る前に userLoader を実行する
}
])
// BrowserRouter:JSXでルートを定義する場合(loader は使えない)
<BrowserRouter>
<Routes>
<Route path="/users/:userId" element={<UserDetail />} />
</Routes>
</BrowserRouter>コードを見ると4つインポートされています。
//Appコンポーネント
import { createBrowserRouter, RouterProvider} from 'react-router-dom'
//UserDetailコンポーネント
import { useLoaderData, LoaderFunctionArgs} from 'react-router-dom'loader を使うにあたって、新しく登場する4つのAPIを先に把握しておきましょう。
それぞれの役割は
| 名前 | 種類 | 役割 |
|---|---|---|
createBrowserRouter | 関数 | ルート設定のオブジェクトを受け取り、ルーターを生成する |
RouterProvider | コンポーネント | createBrowserRouter で作ったルーターをアプリに渡す。BrowserRouter の代わりに使う |
useLoaderData | フック | loader 関数が返したデータをコンポーネントの中で受け取る |
LoaderFunctionArgs | 型(TypeScriptのみ) | loader 関数の引数の型。params(URLパラメータ)や request(リクエスト情報)を含む |
loader はルート定義(どのページを表示)とデータ取得(表示したいデータ)をセットで管理する仕組みのため、オブジェクト形式でルートを定義できる createBrowserRouter が必要になります。
つまり、
ページが表示される前に処理するべきメソッドをloaderとして渡す事ができます。
ここでページ表示前に何のデータを取得したいかコントロールできるわけです。
UserDetail()の内側で使用しているuseLoaderData() フックは loader 関数が返した値をコンポーネントの中で受け取るためのフックです。
まとめると
① createBrowserRouter ルートと loader をセットで定義する
↓
② RouterProvider ① で作ったルーターをアプリ全体に渡す
↓
ユーザーがページにアクセス
↓
loader 関数が実行されデータを取得
(LoaderFunctionArgs で引数に型を付ける ※TSのみ)
↓
③ useLoaderData loader が返したデータをコンポーネントで受け取る
↓
画面が表示されるuseState や useEffect を書かずに、最初からデータが揃った状態でレンダリングできます。
💡再利用性のあるローダー
①一般的にはコンポーネントと同じファイルに置く方法がよく使われます。「このページのデータ取得はここ」とひと目でわかるためです。
②共通で使い回すデータ取得が増えてきたタイミングで以下のように別のフォルダにまとめておくと簡潔で再利用しやすい管理できます。
src/
├── loaders/
│ ├── userLoader.ts
│ ├── blogLoader.ts
│ └── dashboardLoader.ts
├── pages/
│ ├── UserDetail.tsx
│ └── BlogPost.tsx
└── App.tsx実践:ブログアプリケーション
ここまで学んだことを組み合わせて、実際に動くブログアプリを作ります。
「記事一覧ページ」と「記事詳細ページ」を持つシンプルな構成です。
このセクションを読めば、React Routerの主要な機能を組み合わせた、実用的なアプリケーションが自分で作れるようになります。
JavaScript版:
// App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Layout from './components/Layout'
import Home from './pages/Home'
import BlogList from './pages/BlogList'
import BlogPost from './pages/BlogPost'
import About from './pages/About'
import NotFound from './pages/NotFound'
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="blog" element={<BlogList />} />
<Route path="blog/:postId" element={<BlogPost />} />
<Route path="about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
)
}
export default Apppages/BlogList.jsx:
import { Link } from 'react-router-dom'
import { useState, useEffect } from 'react'
function BlogList() {
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts')
.then(res => res.json())
.then(data => {
setPosts(data.slice(0, 10))
setLoading(false)
})
}, [])
if (loading) return <p>読み込み中...</p>
return (
<div>
<h1>ブログ記事一覧</h1>
<ul>
{posts.map(post => (
<li key={post.id}>
<Link to={`/blog/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
</div>
)
}
export default BlogListpages/BlogPost.jsx:
import { useParams, useNavigate } from 'react-router-dom'
import { useState, useEffect } from 'react'
function BlogPost() {
const { postId } = useParams()
const navigate = useNavigate()
const [post, setPost] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
.then(res => res.json())
.then(data => {
setPost(data)
setLoading(false)
})
}, [postId])
if (loading) return <p>読み込み中...</p>
return (
<div>
<button onClick={() => navigate('/blog')}>← 一覧に戻る</button>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
)
}
export default BlogPostTypeScript版:
App.tsx:
// App.tsx
import { type JSX } from 'react'
import './App.css'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import Home from './pages/Home'
import NotFound from './pages/NotFound'
import About from './pages/About'
import Layout from './components/Layout'
import BlogList from './pages/BlogList'
import BlogPost from './pages/BlogPost'
function App(): JSX.Element {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="blog" element={<BlogList />} />
<Route path="blog/:postId" element={<BlogPost />} />
<Route path="about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
)
}
export default App
pages/BlogList.jsx:
import { Link } from 'react-router-dom'
import { useState, useEffect } from 'react'
interface Post {
id: number
title: string
body: string
}
function BlogList() {
const [posts, setPosts] = useState<Post[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts')
.then((res) => res.json())
.then((data) => {
setPosts(data.slice(0, 10))
setLoading(false)
})
}, [])
if (loading) return <p>読み込み中...</p>
return (
<div>
<h1>ブログ記事一覧</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link to={`/blog/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
</div>
)
}
export default BlogList
pages/BlogPost.jsx:
// pages/BlogPost.tsx
import { useParams, useNavigate } from 'react-router-dom'
import { useState, useEffect, type JSX} from 'react'
interface Post {
id: number
title: string
body: string
}
function BlogPost(): JSX.Element {
const { postId } = useParams<{ postId: string }>()
const navigate = useNavigate()
const [post, setPost] = useState<Post | null>(null)
const [loading, setLoading] = useState<boolean>(true)
useEffect(() => {
fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`)
.then(res => res.json())
.then((data: Post) => {
setPost(data)
setLoading(false)
})
}, [postId])
if (loading) return <p>読み込み中...</p>
if (!post) return <p>記事が見つかりません</p>
return (
<div>
<button onClick={() => navigate('/blog')}>← 一覧に戻る</button>
<h1>{post.title}</h1>
<p>{post.body}</p>
</div>
)
}
export default BlogPostこのページで学んだ全てを利用できていませんが、興味があれば自分で変更を加えて使ってみてください。
(例えば、loaderを使ったデータ取得など)
まとめ
この記事では、React Routerを使ったページ遷移の実装方法を一通り学びました。
最初は概念が多くて難しく感じるかもしれませんが、基本的な BrowserRouter・Routes・Route・Link の4つさえ押さえれば、あとは必要に応じて機能を足していくだけです。
重要なポイントの振り返り
BrowserRouterでアプリ全体をラップしてルーティングを有効にするRoutesとRouteでURLとコンポーネントを対応付けるLinkでページ遷移(ページリロードなしで高速に動く)useParamsでURLの:id部分を取得するuseNavigateでコードから任意のページへ移動するuseSearchParamsでURLの?key=valueを読み書きする
主要なHooks一覧
| Hook | 用途 |
|---|---|
useParams | /users/:id のようなURLパラメータを取得 |
useNavigate | コードからページ遷移を実行 |
useSearchParams | ?q=react のようなクエリパラメータを管理 |
useLoaderData | loader 関数が取得したデータを受け取る |
ベストプラクティス
- 共通UIはレイアウトコンポーネントと
<Outlet />でまとめる - 現在のページがわかるよう
NavLinkを活用する - 存在しないURLのために
path="*"の404ページを必ず設定する - 認証が必要なページは
ProtectedRouteパターンで保護する - 画面の階層構造はネストされたルートで表現する
次のステップ: 次回は、APIからデータを取得して表示する実践的なアプリケーションを作ります。React RouterとAPIを組み合わせることで、本格的なWebアプリケーションが作れるようになります!
React Routerは、Reactアプリケーションに欠かせないライブラリです。ぜひ今回のパターンを実際のプロジェクトで試しながら、SPA開発の感覚を身につけていきましょう!


























