デザインについての学習メモブログ

React入門 #13 – React Routerでページ遷移を実装

記事内に広告が含まれています。

React入門 #13 – React Routerでページ遷移を実装

これまでは1つのページだけを表示するコンポーネントを作ってきました。

しかし、実際のWebアプリでは「ホームページ」「プロフィールページ」「お問い合わせページ」など、複数のページを行き来できることが必要です。

そのために使うのが React Router です。

Reactアプリにページ遷移機能を追加するためのライブラリで、現在最も広く使われています。

この記事では、React Routerの基本的な使い方から、実践的なパターンまでステップごとに学んでいきます。

React Routerとは?

「そもそもルーティングって何?」というところから丁寧に解説します。

React Routerがどんなライブラリでどんなことができるのか、全体像をつかんだうえでインストールまで進めましょう。

このセクションを読めば、React Routerを使う理由と基本の考え方が理解できます。

ルーティングってなに?

「ルーティング」とは、URLに応じて表示する画面を切り替える仕組みのことです。

たとえば、

  • https://example.com/ → ホーム画面を表示
  • https://example.com/about → 概要画面を表示
  • https://example.com/contact → お問い合わせ画面を表示

というように、URLが変わるたびに対応するコンポーネントを表示します。

React Routerの主な特徴

  • ページ全体をリロードしないページ遷移(SPA:シングルページアプリケーション)
  • ブラウザの「戻る」「進む」ボタンへの対応
  • URLから値を取り出す(例:/users/123123 部分)
  • ログインしていないと見られないページの制御

インストール

Bash
npm install react-router-dom

インストール後、以下のような警告が表示されることがあります。

Bash
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 を試す

Bash
npm audit fix

これで自動的に安全なバージョンへ更新されます。多くの場合はこれで解消します。

それでも残る場合は様子を見てOK

npm audit fix を実行しても警告が消えないことがあります。

これは「修正バージョンがまだリリースされていない」か「メジャーバージョンアップが必要で自動修正できない」ケースです。

学習・個人開発の段階では、残った警告をそのまま放置しても問題ありません。

本番リリースが近づいたタイミングで改めて確認するようにしましょう。

基本的なルーティング

React Routerを使った、もっともシンプルな構成を学びます。

「URLが変わると別のコンポーネントが表示される」という基本の流れを実際のコードで確認しましょう。

このセクションを読めば、複数ページを切り替える最小構成を自分で書けるようになります。

シンプルなルート設定

まずは一番シンプルな例として、3つのページを切り替えるアプリを作ります。

JavaScript版:

JSX
// 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 App

TypeScript版:

TSX
// 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つだけを表示する
RouteURLのパス(path)と表示するコンポーネント(element)を対応付ける
Linkページ遷移のためのリンク。<a> タグと違い、ページ全体をリロードしない

ざっくりと説明すると

Routesの内側にあるRouteコンポーネントのpathプロパティに一致するurlにアクセスしたら、elementプロパティで指定されたコンポーネントが表示される」

💡 なぜ <a> タグではなく <Link> を使うの?

<a> タグはクリックするとページ全体を読み込み直します。<Link> を使うと必要な部分だけ更新されるため、表示が速く、スムーズな操作感を実現できます。

レイアウトコンポーネント

ヘッダーやフッターのように「すべてのページで共通して表示したいUI」を、ページごとにコピー&ペーストするのは大変です。

このセクションでは、共通部分を一か所にまとめる「レイアウトコンポーネント」パターンを学びます。

このセクションを読めば、コードの重複をなくして、保守しやすいアプリ構成が作れるようになります。

共通レイアウトの作成

JavaScript版:

JSX
// 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コンポーネント内に書いていた各ページのコンポーネントは別のファイルに切り出せるはずです。

JSX
// 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版:

TSX
// 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 Layout

URLパラメータ

/users/123/blog/react/456 のように、URLの一部に可変の値を含む「動的なURL」を扱う方法を学びます。

このセクションを読めば、ユーザーIDや記事IDなどをURLから取り出して、対応するデータを表示できるようになります。

動的ルート

JavaScript版:

JSX
// 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 UserProfile

App.jsxでの設定:

JSX
<Routes>
  <Route path="/users/:userId" element={<UserProfile />} />
</Routes>

// 使用例
<Link to="/users/123">ユーザー123</Link>
<Link to="/users/456">ユーザー456</Link>

TypeScript版:

TSX
// 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版:

JSX
// 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
JSX
// ルート設定
<Route path="/blog/:category/:postId" element={<BlogPost />} />

// 使用例
<Link to="/blog/react/123">React記事123</Link>

TypeScript版:

TSX
// 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行目の以下の部分に注目してください。

TSX
{ category: string; postId: string }

TypeScriptのオブジェクト型の中ではどちらも正しく、同じ意味です。

慣習的にオブジェクトの「」には ”,、オブジェクトの「」には” ; を使うことが多いです。

TypeScriptの公式ドキュメントでも型定義には ; が使われています。

💡 string | undefinedとしなくてもエラーないの?

TSX
useParams<{ category: string; postId: string }>()

TypeScriptの仕様で、interface インライン型 {} の扱いが異なります。

インライン型はインデックスシグネチャを暗黙的にあるとみなします。

TSX
// インライン型 → 暗黙的にインデックスシグネチャを持つとみなされる → エラーなし
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を更新する関数をセットで返します。

JSX
const [searchParams, setSearchParams] = useSearchParams()
//     ↑ 読み取り用        ↑ 書き込み用

読み取り:searchParams.get('キー名')

JSX
// URL が /search?q=react&page=2 のとき
searchParams.get('q')     // → 'react'
searchParams.get('page')  // → '2'
searchParams.get('sort')  // → null(存在しないキーはnullになる)

書き込み:setSearchParams({ キー: 値 })

JSX
// ボタンを押すと URL が /search?q=react&page=2 に変わる
setSearchParams({ q: 'react', page: '2' })

💡 useState との違い

useState はコンポーネントの中だけに状態を持ちますが、useSearchParams はURLに状態を持ちます。

URLに状態があると

  • 「ページをリロードしても検索条件が保持される」
  • 「検索結果のURLをそのまま共有できる」

といったメリットがあります。

サンプル

JavaScript版:

JSX
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
TSX
// ルート設定
<Route path="search" element={<SearchResults />} />

// 使用例
<Link to="/search?q=react&page=1">検索</Link>

TypeScript版:

TSX
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

useNavigate は、コードの中からページ遷移を実行するためのフックです。

返ってくる navigate 関数を呼び出すだけで、任意のページへ移動できます。

JSX
const navigate = useNavigate()

指定したパスへ移動する

JSX
navigate('/dashboard')       // /dashboard へ移動
navigate('/users/123')       // /users/123 へ移動

履歴を残さずに移動する(replace

JSX
navigate('/dashboard', { replace: true })

通常の移動はブラウザの履歴に積まれるため「戻る」ボタンで前のページに戻れます。

replace: true を指定すると履歴を上書きするため戻れなくなります。

ログイン後のリダイレクトなど「ログイン画面に戻ってほしくない」場面で使います。

履歴を使って移動する

JSX
navigate(-1)   // 1つ前のページへ(ブラウザの「戻る」と同じ)
navigate(1)    // 1つ次のページへ(ブラウザの「進む」と同じ)
navigate(-2)   // 2つ前のページへ

💡 Link との使い分け

クリックで移動するだけなら <Link> で十分です。

「ログイン成功後」「フォーム送信後」など、処理の結果としてページを移動したいときに useNavigate を使いましょう。

使用例

JavaScript版:

JSX
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版:

TSX
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 LoginForm

404ページ(Not Found)

存在しないURLにアクセスされたとき、何も表示されないままでは利用者が困惑します。

「ページが見つかりません」と伝える404ページの作り方を学びましょう。

このセクションを読めば、どんなURLにアクセスされても適切にエラーページを表示できるようになります。

404ページとは?

「404」とはWebの世界で「リソースが見つからない」を意味するHTTPステータスコードです。

たとえば「ユーザーがURLを打ち間違えたり」、「削除されたページにアクセスしたりしたとき」に返されます。

React Routerでは path="*" を使って「どのルートにも一致しなかった場合」を受け取ることができます。

JSX
/          → Home にマッチ
/aboutAbout にマッチ
/abcxyzどこにもマッチしないpath="*" が受け取る

💡 path="*" は必ず一番最後に書く

Routes は上から順にURLを照合し、最初にマッチしたルートだけを表示します。

path="*" を先に書くとすべてのURLがここにマッチしてしまうため、必ず一番下に置きましょう。

JavaScript版:

JSX
// pages/NotFound.jsx
import { Link } from 'react-router-dom'

function NotFound() {
  return (
    <div>
      <h1>404 - ページが見つかりません</h1>
      <p>お探しのページは存在しません。</p>
      <Link to="/">ホームに戻る</Link>
    </div>
  )
}

export default NotFound

App.jsxでの設定:

JSX
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  {/* その他のルート */}
  
  {/* !!!!”必ず最後”に404ルートを配置 */}
  <Route path="*" element={<NotFound />} />
</Routes>

TypeScript版:

TSX
// 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 – アクティブなリンクのスタイリング

ナビゲーションメニューで「今どのページにいるか」を視覚的にわかりやすく表示したいことがあります。

NavLink を使うと、現在表示中のページに対応するリンクに自動でスタイルを適用できます。

このセクションを読めば、現在地がひと目でわかるナビゲーションメニューを実装できるようになります。

NavLinkの使用

JavaScript版:

JSX
// /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 Navigation

Layoutコンポーネント内のheader要素内に実装してください。

JSX
・・・省略
import Navigation from './Navigation'

function Layout(): JSX.Element {
  return (
    <div>
      <header>
        <Navigation />
        ・・・省略

CSS:

CSS
/* 今回はApp.cssにもで追加してください。*/
.active {
  font-weight: bold;
  color: blue;
  text-decoration: underline;
}

TypeScript版:

TSX
//  /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

NavLink と Link の違い

どちらもページ遷移のためのコンポーネントですが、用途が異なります。

LinkNavLink
ページ遷移
現在のURLと一致したか検知
アクティブ時のスタイル適用
JSX
// Link:シンプルなリンク。アクティブ状態は関知しない
<Link to="/about">概要</Link>

// NavLink:現在のURLと一致したとき isActive が true になる
<NavLink to="/about">概要</NavLink>

使い分けの目安

  • ナビゲーションメニューのリンク → NavLink(現在地を強調したいため)
  • 記事内のリンク、ボタン代わりのリンクなど → Link(アクティブ状態が不要なため)

ネストされたルート

ダッシュボードのような画面では、「ダッシュボードの中にプロフィール・設定・メッセージなどのサブページがある」という入れ子構造が必要です。

このセクションでは、ルートを入れ子にして階層的なページ構成を実現する方法を学びます。

このセクションを読めば、複雑な画面構成でも整理されたルート設定が書けるようになります。

複雑なルート構造

JavaScript版:

JSX
// 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
JSX
// pages/DashboardProfile.jsx
function DashboardProfile() {
  return <h2>プロフィール</h2>
}

export default DashboardProfile
JSX
// pages/DashboardSettings.jsx
function DashboardSettings() {
  return <h2>設定</h2>
}

export default DashboardSettings
JSX
// pages/DashboardMessages.jsx
function DashboardMessages() {
  return <h2>メッセージ</h2>
}

export default DashboardMessages
JSX
// 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/messages

TypeScript版:

TSX
// 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の構造とコンポーネントの構造が対応している」という点です。

JSX
URL:       /dashboard/profile
           ↑          ↑
           親ルート    子ルート
コンポーネント: <Dashboard> の中に <DashboardProfile> が表示される

💡 どんなときに使うか

画面の一部だけが切り替わるUIに向いています。たとえばダッシュボードでは、ヘッダーやサイドメニューは共通で、メインエリアだけがプロフィール・設定・メッセージに切り替わります。

レイアウトコンポーネントとの違い

「サイト全体で共通のヘッダー・フッター」ならレイアウトコンポーネント、「特定のページ内でさらにサブページに分かれる」ならネストされたルートと使い分けましょう。

JSX
<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版:

JSX
// 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
JSX
// 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

使用例:

JSX
// 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版:

TSX
// 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
JSX
// 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

着眼点

保護されたルートのポイントは「認証チェックをコンポーネントとして切り出す」ことです。

JSX
ユーザーが /dashboard にアクセス

  ProtectedRoute が認証状態を確認

  ログイン済み
  ┌──── Yes ────┐
  │             │
  ↓             ↓
<Dashboard />  <Navigate to="/login" />
を表示          にリダイレクト

ProtectedRoute を作っておくと、保護したいページを増やすときに認証ロジックをコピーせず、単純に ProtectedRoute で包むだけで済みます。

JSX
// 保護するページを増やすのが簡単
<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 は、ブラウザの履歴を上書きするオプションです。

これがないと「ダッシュボード→ログインページ」の履歴が残り、ログイン後に「戻る」ボタンを押すとダッシュボードに戻ろうとして再びリダイレクトされるループが発生します。

useNavigate と Navigate の違い

セクション6で学んだ useNavigate と、ProtectedRoute で使っている <Navigate /> はどちらも「ページ遷移」のためのものですが、使う場面が異なります。

useNavigate<Navigate />
種類フック(Hook)コンポーネント
使う場面イベント処理の中で移動したいときレンダリング中に移動したいとき
使い方navigate('/path') を呼び出すJSXとして返す
JSX
// useNavigate:ボタンを押したときなど「処理の中」で使う
function Login() {
  const navigate = useNavigate()

  const handleLogin = () => {
    navigate('/dashboard')  // 処理の結果として移動
  }

  return <button onClick={handleLogin}>ログイン</button>
}
JSX
// 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版:

JSX
//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>
  )
}
JSX
//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版:

TSX
//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>
  )
}
JSX
//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 を使って「コンポーネントが表示されてから取得する」という流れでした。

JSX
// これまでの書き方(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 を使うと「ページに移動する前にデータを取得する」という流れに変わります。

JSX
useEffect の場合ページ表示データ取得再レンダリングローディングが挟まる
loader の場合データ取得ページ表示最初からデータがある

また BrowserRouter ではなく createBrowserRouter を使う点も重要です。

JSX
// 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つインポートされています。

JSX
//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 関数が返した値をコンポーネントの中で受け取るためのフックです。

まとめると

JSX
createBrowserRouter  ルートと loader をセットで定義する

RouterProviderで作ったルーターをアプリ全体に渡す

  ユーザーがページにアクセス

  loader 関数が実行されデータを取得
LoaderFunctionArgs で引数に型を付けるTSのみ

useLoaderData        loader が返したデータをコンポーネントで受け取る

  画面が表示される

useStateuseEffect を書かずに、最初からデータが揃った状態でレンダリングできます。

💡再利用性のあるローダー

一般的にはコンポーネントと同じファイルに置く方法がよく使われます。「このページのデータ取得はここ」とひと目でわかるためです。

共通で使い回すデータ取得が増えてきたタイミングで以下のように別のフォルダにまとめておくと簡潔で再利用しやすい管理できます。

Bash
src/
├── loaders/
   ├── userLoader.ts
   ├── blogLoader.ts
   └── dashboardLoader.ts
├── pages/
   ├── UserDetail.tsx
   └── BlogPost.tsx
└── App.tsx

実践:ブログアプリケーション

ここまで学んだことを組み合わせて、実際に動くブログアプリを作ります。

「記事一覧ページ」と「記事詳細ページ」を持つシンプルな構成です。

このセクションを読めば、React Routerの主要な機能を組み合わせた、実用的なアプリケーションが自分で作れるようになります。

JavaScript版:

JSX
// 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 App

pages/BlogList.jsx:

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 BlogList

pages/BlogPost.jsx:

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 BlogPost

TypeScript版:

App.tsx:

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:

TSX
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:

TSX
// 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 でアプリ全体をラップしてルーティングを有効にする
  • RoutesRoute でURLとコンポーネントを対応付ける
  • Link でページ遷移(ページリロードなしで高速に動く)
  • useParams でURLの :id 部分を取得する
  • useNavigate でコードから任意のページへ移動する
  • useSearchParams でURLの ?key=value を読み書きする

主要なHooks一覧

Hook用途
useParams/users/:id のようなURLパラメータを取得
useNavigateコードからページ遷移を実行
useSearchParams?q=react のようなクエリパラメータを管理
useLoaderDataloader 関数が取得したデータを受け取る

ベストプラクティス

  • 共通UIはレイアウトコンポーネントと <Outlet /> でまとめる
  • 現在のページがわかるよう NavLink を活用する
  • 存在しないURLのために path="*" の404ページを必ず設定する
  • 認証が必要なページは ProtectedRoute パターンで保護する
  • 画面の階層構造はネストされたルートで表現する

次のステップ: 次回は、APIからデータを取得して表示する実践的なアプリケーションを作ります。React RouterとAPIを組み合わせることで、本格的なWebアプリケーションが作れるようになります!

React Routerは、Reactアプリケーションに欠かせないライブラリです。ぜひ今回のパターンを実際のプロジェクトで試しながら、SPA開発の感覚を身につけていきましょう!