React Routerの使い方:ルーティングと画面遷移の基本(v8・v7とreact-router-domの違いも)
目次
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 でした。
[email protected] /work/old
`-- [email protected]
`-- [email protected]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-routerpackage.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 で開いた最初の画面です。
共通のレイアウトは 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」が赤くなりました。
useParams で URL のパラメータを読む
path="users/:id" の :id の部分は、useParams() で読めます。
const { id } = useParams()/users/42 を開けば id は "42" です。
数字の URL でも、受け取る値は文字列なので、計算に使うときは Number(id) のように変換します。
「次のユーザーへ」を押すと /users/2 に移りました。
NavLink の「ユーザー1」は /users/1 を指しているので、/users/2 では active が付きません。
useNavigate でコードから移動する
ボタンを押したときや、フォームを送ったあとに移動したいときは useNavigate を使います。
const navigate = useNavigate()
<button onClick={() => navigate('/')}>ホームに戻る</button>「ホームに戻る」を押すと、/ のホームに戻りました。
path="*" で 404 ページを出す
どのルートにも当てはまらない URL は、path="*" のルートで受けられます。
ブラウザで /nothing を直接開くと、NotFound が表示されました!
* のルートも Layout の中に入れているので、ナビゲーションはそのまま残ります。
MemoryRouter でルーティングをテストする
ルーティングが思った通りに動くかは、テストでも確かめられます。
テストでは BrowserRouter の代わりに MemoryRouter を使い、initialEntries で最初に開く URL を指定します。
Vitest と Testing Library を入れます。
npm install -D vitest jsdom @testing-library/react @testing-library/user-event @testing-library/jest-domvite.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のバージョンの切り替え方法も参考にしてください。




