ホーム>React>React Routerの使い方:ルーティングと画面遷移の基本(v8・v7とreact-router-domの違いも)
React

React Routerの使い方:ルーティングと画面遷移の基本(v8・v7とreact-router-domの違いも)

いつもご利用ありがとうございます。
この記事にはアフィリエイト広告を含む広告が掲載されており、その広告費によって運営しています。
Amazonのアソシエイトとして、ララテックは適格販売により収入を得ています。

React Routerの使い方を、ルートの定義・Linkでの画面遷移・URLパラメータ・useNavigate・404ページの順に解説します。React Router 8.5とVite・React 19.3で実際に動かした画面とテスト結果つきで、v7やreact-router-domとの違いも確かめました。

React で「/about を開いたら会社概要、/users/1 を開いたらユーザー1のページ」のように、URL ごとに画面を切り替えたいときに使うのが React Router です。

この記事では、小さなサイトを1つ作りながら、React Router のルーティングと画面遷移の基本を順番に見ていきます。

コードは、2026年11月に Docker の node:22 のコンテナで実際に動かし、画面とテストの結果を確かめたものです。

確かめた環境は次の通りです。

  • Node.js 22.23.4・npm 10.9.9
  • Vite 8.3.7・React 19.3.1
  • react-router 8.5.1
  • Vitest 5.1.0・@testing-library/react 16.3.3・@testing-library/user-event 14.6.9・jsdom 31.0.0
  • 画面の確認は Chromium 156

React そのものの始め方は、React入門チュートリアル:ViteでToDoアプリを作って使い方を覚えるにまとめています。

今の最新は v8 で、react-router-dom は v7 まで

検索すると react-router-dom を入れる記事と、react-router を入れる記事が混ざっていて迷いますよね?

2026年11月11日に npm で最新版を見たところ、次の通りでした。

$ npm view react-router version
8.5.1

$ npm view react-router-dom dist-tags
{
  ...
  'version-6': '6.30.6',
  latest: '7.18.5'
}

react-router は 8.5.1 なのに、react-router-dom は 7.18.5 が最新です。

試しに react-router-dom@8 を入れようとすると、見つからないと言われました。

npm error notarget No matching version found for react-router-dom@8.

つまり、npm install react-router-dom と打つと v7 が入ります。

実際に空のフォルダで入れてみると、中身の react-router も 7.18.5 でした。

v7 では react-router-dom から BrowserRouter や Routes を import できました。

BrowserRouter: function Routes: function

これから始めるなら、react-router を入れて、import も react-router から書けば OK です。

この記事のコードは、すべて react-router から import しています。

インストール

Vite で React のプロジェクトを作り、react-router を足します。

npm create vite@latest app -- --template react
cd app
npm install
npm install react-router

package.json の依存はこうなりました。

"dependencies": {
  "react": "^19.3.1",
  "react-dom": "^19.3.1",
  "react-router": "^8.5.1"
}

BrowserRouter でアプリを包む

まず src/main.jsx で、アプリ全体を BrowserRouter で包みます。

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </StrictMode>,
)

これを忘れると、画面が真っ白になります。

Router で包まずに <App /> だけを描画したところ、次のエラーが出ました。

useRoutes() may be used only in the context of a <Router> component.

このエラーが出たら、main.jsx の BrowserRouter を最初に確かめましょう。

Routes と Route でルートを定義する

src/App.jsx に、URL と表示するコンポーネントの対応を書きます。

import { Routes, Route, Link, NavLink, Outlet, useParams, useNavigate } from 'react-router'
import './App.css'

function Layout() {
  return (
    <div className="layout">
      <nav>
        <NavLink to="/">ホーム</NavLink>
        <NavLink to="/about">このサイトについて</NavLink>
        <NavLink to="/users/1">ユーザー1</NavLink>
      </nav>
      <main>
        <Outlet />
      </main>
    </div>
  )
}

function Home() {
  return <h1>ホーム</h1>
}

function About() {
  return <h1>このサイトについて</h1>
}

function User() {
  const { id } = useParams()
  const navigate = useNavigate()
  return (
    <>
      <h1>ユーザー {id} のページ</h1>
      <Link to={`/users/${Number(id) + 1}`}>次のユーザーへ</Link>
      <button onClick={() => navigate('/')}>ホームに戻る</button>
    </>
  )
}

function NotFound() {
  return <h1>ページが見つかりません</h1>
}

export default function App() {
  return (
    <Routes>
      <Route element={<Layout />}>
        <Route index element={<Home />} />
        <Route path="about" element={<About />} />
        <Route path="users/:id" element={<User />} />
        <Route path="*" element={<NotFound />} />
      </Route>
    </Routes>
  )
}

Route の path が URL、element がそのときに表示するコンポーネントです。

index は親と同じ URL(ここでは /)で表示するルートです。

npm run dev で開いた最初の画面です。

ナビゲーションに「ホーム」「このサイトについて」「ユーザー1」が並び、今いる「ホーム」だけが赤い文字になっている。下に大きく「ホーム」と表示されている

共通のレイアウトは Outlet で

上のコードでは、path のない Route に Layout を指定し、その中にページのルートを入れています。

Layout の <Outlet /> の場所に、URL に合った子のルートが表示されます。

ナビゲーションのように全ページで共通の部分は、こうして1か所に書けます。

Link と NavLink で画面遷移する

ページの移動には <a> ではなく Link を使います。

Link だとページ全体を読み込み直さずに、React の中で画面だけが切り替わります。

NavLink は Link に「今いるページなら active クラスを付ける」機能を足したものです。

CSS で a.active に色を付けておくと、今いるページが分かるナビゲーションになります。

nav a.active {
  color: #e11d48;
  font-weight: bold;
}

ナビの「ユーザー1」を押すと、URL が http://localhost:5173/users/1 に変わり、ナビの「ユーザー1」が赤くなりました。

URLが/users/1の画面。ナビの「ユーザー1」が赤い太字になり、見出しに「ユーザー 1 のページ」、その下に「次のユーザーへ」のリンクと「ホームに戻る」のボタンがある

useParams で URL のパラメータを読む

path="users/:id" の :id の部分は、useParams() で読めます。

const { id } = useParams()

/users/42 を開けば id は "42" です。

数字の URL でも、受け取る値は文字列なので、計算に使うときは Number(id) のように変換します。

「次のユーザーへ」を押すと /users/2 に移りました。

URLが/users/2の画面。見出しは「ユーザー 2 のページ」になり、ナビの「ユーザー1」は赤くない元の色に戻っている

NavLink の「ユーザー1」は /users/1 を指しているので、/users/2 では active が付きません。

useNavigate でコードから移動する

ボタンを押したときや、フォームを送ったあとに移動したいときは useNavigate を使います。

const navigate = useNavigate()

<button onClick={() => navigate('/')}>ホームに戻る</button>

「ホームに戻る」を押すと、/ のホームに戻りました。

path="*" で 404 ページを出す

どのルートにも当てはまらない URL は、path="*" のルートで受けられます。

ブラウザで /nothing を直接開くと、NotFound が表示されました!

URLが/nothingの画面。ナビの下に「ページが見つかりません」と表示されている

* のルートも Layout の中に入れているので、ナビゲーションはそのまま残ります。

MemoryRouter でルーティングをテストする

ルーティングが思った通りに動くかは、テストでも確かめられます。

テストでは BrowserRouter の代わりに MemoryRouter を使い、initialEntries で最初に開く URL を指定します。

Vitest と Testing Library を入れます。

npm install -D vitest jsdom @testing-library/react @testing-library/user-event @testing-library/jest-dom

vite.config.js に test の設定を足します。

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    setupFiles: './src/setupTests.js',
  },
})

src/setupTests.js はこうしました。

import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach } from 'vitest'

afterEach(() => {
  cleanup()
})

最初は cleanup() を書いていなかったので、6本中5本が「見出しが複数ある」という理由で落ちました。

前のテストで描画した画面が残ったままになっていたためです。

Vitest の globals を有効にしていないときは、afterEach で cleanup() を呼ぶ必要がありました。

テスト本体の src/App.test.jsx です。

import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { MemoryRouter } from 'react-router'
import { describe, expect, test } from 'vitest'
import App from './App.jsx'

function renderAt(path) {
  return render(
    <MemoryRouter initialEntries={[path]}>
      <App />
    </MemoryRouter>,
  )
}

describe('ルーティング', () => {
  test('/ ではホームが出る', () => {
    renderAt('/')
    expect(screen.getByRole('heading')).toHaveTextContent('ホーム')
  })

  test('/users/42 では URL の id が読める', () => {
    renderAt('/users/42')
    expect(screen.getByRole('heading')).toHaveTextContent('ユーザー 42 のページ')
  })

  test('定義していない URL では * のルートが出る', () => {
    renderAt('/nothing')
    expect(screen.getByRole('heading')).toHaveTextContent('ページが見つかりません')
  })

  test('Link を押すと画面が切り替わる', async () => {
    renderAt('/')
    await userEvent.click(screen.getByRole('link', { name: 'このサイトについて' }))
    expect(screen.getByRole('heading')).toHaveTextContent('このサイトについて')
  })

  test('今いるページの NavLink には active が付く', () => {
    renderAt('/about')
    expect(screen.getByRole('link', { name: 'このサイトについて' })).toHaveClass('active')
    expect(screen.getByRole('link', { name: 'ホーム' })).not.toHaveClass('active')
  })

  test('useNavigate でホームに戻る', async () => {
    renderAt('/users/1')
    await userEvent.click(screen.getByRole('link', { name: '次のユーザーへ' }))
    expect(screen.getByRole('heading')).toHaveTextContent('ユーザー 2 のページ')
    await userEvent.click(screen.getByRole('button', { name: 'ホームに戻る' }))
    expect(screen.getByRole('heading')).toHaveTextContent('ホーム')
  })
})

npx vitest run の結果です。

 RUN  v5.1.0 /work/app

 ✓ src/App.test.jsx (6 tests) 92ms

 Test Files  1 passed (1)
      Tests  6 passed (6)

6本とも通りました (๑•̀ㅂ•́)و✧

公開するときは、どの URL でも index.html を返す設定が要る

npm run build で作った dist/index.html の中身は、<div id="root"></div> が空のままです。

画面は、ブラウザで JavaScript が動いてから作られます。

そのため、/users/1 を直接開いたときにも、サーバーが index.html を返さないと React Router まで届きません。

dist を静的なファイルとして配って確かめました(serve 14.2.7)。

# そのまま配った場合(npx serve dist)
/          200
/users/1   404

# どの URL でも index.html を返す場合(npx serve -s dist)
/          200
/users/1   200

そのまま配ると、トップページから「ユーザー1」を押して移動するのは動くのに、/users/1 を直接開いたりリロードしたりすると 404 になります。

公開先のサーバーで、存在しない URL を index.html に向ける設定(SPA のフォールバック)を入れておきましょう。

参考:v7 から v8 で変わったこと

公式のアップグレードガイドによると、React Router v8 では react-router-dom のパッケージがなくなり、DOM 向けの API は react-router/dom から、それ以外は react-router から import するようになったとのことです。

// v7 まで
import { Link, useLocation } from "react-router-dom";

// v8
import { Link, useLocation } from "react-router";
import { RouterProvider } from "react-router/dom";

同じガイドでは、v8 に上げる前に Node.js を 22.22 以上、React と React DOM を 19.2.7 以上にしておくよう書かれています。

フレームワークモードでは Vite 7 以上も必要とのことです。

出典:Upgrading from v7(React Router 公式)

参考:React Router の3つの使い方

公式ドキュメントでは、React Router の使い方を「宣言型(Declarative)」「データ(Data)」「フレームワーク(Framework)」の3つのモードに分けて説明しているようです。

この記事で使った BrowserRouter・Routes・Route の書き方は、宣言型にあたります。

データの読み込み(loader)やサーバーでの描画まで任せたい場合は、データモードやフレームワークモードを選ぶとよいようです。

出典:Picking a Mode(React Router 公式)

まとめ

React Router の基本は、次の5つでした。

  • BrowserRouter でアプリを包む(忘れると useRoutes() may be used only in the context of a <Router> component.)
  • Routes と Route で URL とコンポーネントを対応させる
  • Link・NavLink で移動し、useParams で URL の値を読む
  • useNavigate でコードから移動し、path="*" で 404 を出す
  • 公開するときは、どの URL でも index.html を返す

react-router-dom を入れると v7 になるので、古い記事のコードを写すときは import 元に気をつけてくださいね!

React の環境作りで Node.js のバージョンを切り替えたいときは、Node.jsのバージョンの切り替え方法も参考にしてください。

フィードバックのお願い
この記事のフィードバックがありましたらYoutubeの適当な動画にコメントしていただいたり、お問い合わせからご連絡ください。