- ■はじめに
- ■環境
- ■React Routerのインストール
- ■基本的なルーティングの定義
- ■ネストルーティングの定義
- ■ルーティング定義の分割
- ■URLパラメータの活用
- ■クエリパラメータの活用
- ■stateを使ったページ間のデータ受け渡し
- ■JavaScriptでのページ遷移
- ■まとめ
- ■最後に
■はじめに
こんにちは!
iimonでエンジニアをしています「しらみず」です。
本記事はiimon Advent Calendar 202510日目の記事となります!
iimonに入社して早3年経ち、時の流れが早いなーと感じています。
最近、弊社で提供しているWebサービスをVue2→Reactへリプレースするプロジェクトが進行しているのですが、レビュアーをしている自分があまりReactのことを理解してないなーと感じていました。
少しでも良いレビューをしたり、今後の保守・運用面を考えて、早急に身に付けないとなーと感じています。
ということで、亀の歩みながら、ほそぼそと学んでいるのですが、その中でも今回はReact Router v7を使ったSPA開発のためのルーティング定義について書いていきたいと思います。
■環境
"dependencies": { "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^7.10.1" }
今回は、上に記載しているライブラリとバージョンで開発していこうと思います。
特にReact Router v7 または v6とv5では使い方が異なる部分も多いため、React Router v6以降を使っていただければと思います。
以下のリポジトリにサンプルコードを入れていますので、ご自由に利用ください。
■React Routerのインストール
npm install react-router-dom
まずは必要なパッケージをインストールします。
React Router v6以降では、react-router-domをインストールするだけで、内部依存としてreact-routerも自動的にインストールされます。
そのため、react-router-domだけインストールすれば大丈夫です。
■基本的なルーティングの定義
◆コンポーネント
src/components/Home.jsx
export const Home = () => { return ( <div className="bg-white rounded-lg shadow p-6"> <h1 className="text-3xl font-bold text-gray-900 mb-4"> ようこそ、テックブログへ </h1> <p className="text-gray-600 leading-relaxed"> 最新の技術記事やチュートリアルを発信しています。 記事一覧から気になる記事をチェックしてみてください。 </p> </div> ); };
src/components/Articles.jsx
export const Articles = () => { return ( <div className="bg-white rounded-lg shadow p-6"> <h1 className="text-3xl font-bold text-gray-900 mb-4">記事一覧</h1> <p className="text-gray-600">記事の一覧がここに表示されます</p> </div> ); };
src/components/Contact.jsx
export const Contact = () => { return ( <div className="bg-white rounded-lg shadow p-6"> <h1 className="text-3xl font-bold text-gray-900 mb-4">お問い合わせ</h1> <form className="space-y-4"> <div> <label className="block text-sm font-medium text-gray-700 mb-1"> お名前 </label> <input type="text" className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1"> メッセージ </label> <textarea rows={4} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div> <button type="submit" className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors" > 送信 </button> </form> </div> ); };
3つのコンポーネントファイルを作成しました。
各コンポーネントファイルは、上のサンプルコードのようにしています。
- HomeコンポーネントがTopページを作るコンポーネントです。
- Articlesコンポーネントが記事一覧を表示するコンポーネントです。
- Contactコンポーネントが問い合わせフォームを表示するコンポーネントです。
src/App.jsx
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; import { Home } from './components/Home'; import { Articles } from './components/Articles'; import { Contact } from './components/Contact'; function App() { return ( <div className="min-h-screen bg-gray-50"> <BrowserRouter> <nav className="bg-white shadow-sm"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="flex justify-between h-16"> <div className="flex space-x-8"> <Link to="/" className="inline-flex items-center px-1 pt-1 text-gray-900 hover:text-blue-600 transition-colors" > ホーム </Link> <Link to="/articles" className="inline-flex items-center px-1 pt-1 text-gray-900 hover:text-blue-600 transition-colors" > 記事一覧 </Link> <Link to="/contact" className="inline-flex items-center px-1 pt-1 text-gray-900 hover:text-blue-600 transition-colors" > お問い合わせ </Link> </div> </div> </div> </nav> <main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8"> <Routes> <Route path="/" element={<Home />} /> <Route path="/articles" element={<Articles />} /> <Route path="/contact" element={<Contact />} /> </Routes> </main> </BrowserRouter> </div> ); } export default App;

シンプルなルーティングを定義してみます。
「ホーム」「記事一覧」「お問い合わせ」の3ページを持つアプリケーションを作成します。
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';でimportしたコンポーネントが使われている部分がReact Routerの機能です。
ページを表示すると、ヘッダーにリンクが表示されます。
各リンクをクリックすることで、ルーティングに紐付いているコンポーネントをSPAでレンダリングすることができます。
使っているReact Routerコンポーネントの役割は以下になります。
<BrowserRouter>アプリ全体に「ルーティングの仕組み」を提供するルートコンポーネントです。
<Link>React Routerでページ遷移を行うためのLinkコンポーネントです。
レンダリングされたページのHTML上は
<a>タグですが、ページを再読み込みせずSPAとして動作する点が異なります。<Routes>と<Route>
■ネストルーティングの定義
例えば、「サイドバーを固定したまま、メインコンテンツだけ切り替えたい」というケースで使えるのがネストルーティングです。
◆パスを完全指定した場合のルーティング

src/components/Articles.jsx
export const Articles = () => { return ( <div className="bg-white rounded-lg shadow p-6"> <h1 className="text-3xl font-bold text-gray-900 mb-4">記事管理</h1> <p className="text-gray-600"> 記事管理のトップページです。左のナビゲーションから記事一覧や記事編集にアクセスできます。 </p> </div> ); };
src/components/ArticleList.jsx
export const ArticleList = () => { const articles = [ { id: 1, title: 'React Hooksの基礎', status: '公開', date: '2024-12-01' }, { id: 2, title: 'TypeScriptでの型定義', status: '下書き', date: '2024-12-02' }, { id: 3, title: 'Next.jsで始めるSSR', status: '公開', date: '2024-12-03' }, ]; return ( <div className="bg-white rounded-lg shadow p-6"> <h1 className="text-3xl font-bold text-gray-900 mb-6">記事一覧</h1> <div className="overflow-x-auto"> <table className="min-w-full divide-y divide-gray-200"> <thead className="bg-gray-50"> <tr> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> ID </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> タイトル </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> 公開日 </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> ステータス </th> </tr> </thead> <tbody className="bg-white divide-y divide-gray-200"> {articles.map((article) => ( <tr key={article.id} className="hover:bg-gray-50 transition-colors"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{article.id}</td> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"> {article.title} </td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> {article.date} </td> <td className="px-6 py-4 whitespace-nowrap"> <span className={`px-2 py-1 text-xs font-medium rounded ${ article.status === '公開' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800' }`} > {article.status} </span> </td> </tr> ))} </tbody> </table> </div> </div> ); };
src/components/ArticleEdit.jsx
import { useState } from 'react'; export const ArticleEdit = () => { const [title, setTitle] = useState(''); const [content, setContent] = useState(''); const [category, setCategory] = useState(''); return ( <div className="bg-white rounded-lg shadow p-6"> <h1 className="text-3xl font-bold text-gray-900 mb-6">記事編集</h1> <form className="space-y-6"> <div> <label className="block text-sm font-medium text-gray-700 mb-1">タイトル</label> <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="記事のタイトルを入力" /> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1">カテゴリ</label> <select value={category} onChange={(e) => setCategory(e.target.value)} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" > <option value="">選択してください</option> <option value="React">React</option> <option value="TypeScript">TypeScript</option> <option value="Next.js">Next.js</option> </select> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1">本文</label> <textarea value={content} onChange={(e) => setContent(e.target.value)} rows={10} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="記事の本文を入力" /> </div> <div className="flex gap-3"> <button type="submit" className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors" > 保存 </button> <button type="button" className="px-6 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors" > キャンセル </button> </div> </form> </div> ); };
src/App.jsx
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; import { Home } from './components/Home'; import { Articles } from './components/Articles'; import { ArticleList } from './components/ArticleList'; import { ArticleEdit } from './components/ArticleEdit'; import { Contact } from './components/Contact'; function App() { return ( <div className="min-h-screen bg-gray-50"> <BrowserRouter> <nav className="bg-white shadow-sm"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="flex justify-between h-16"> <div className="flex space-x-8"> <Link to="/" className="inline-flex items-center px-1 pt-1 text-gray-900 hover:text-blue-600 transition-colors" > ホーム </Link> <Link to="/articles" className="inline-flex items-center px-1 pt-1 text-gray-900 hover:text-blue-600 transition-colors" > 記事管理 </Link> <Link to="/articles/list" className="inline-flex items-center px-1 pt-1 text-gray-900 hover:text-blue-600 transition-colors" > 記事一覧 </Link> <Link to="/articles/edit" className="inline-flex items-center px-1 pt-1 text-gray-900 hover:text-blue-600 transition-colors" > 記事編集 </Link> <Link to="/contact" className="inline-flex items-center px-1 pt-1 text-gray-900 hover:text-blue-600 transition-colors" > お問い合わせ </Link> </div> </div> </div> </nav> <main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8"> <Routes> <Route path="/" element={<Home />} /> <Route path="/articles" element={<Articles />} /> <Route path="/articles/list" element={<ArticleList />} /> <Route path="/articles/edit" element={<ArticleEdit />} /> <Route path="/contact" element={<Contact />} /> </Routes> </main> </BrowserRouter> </div> ); } export default App;
まずは、ルーティングをネストしないで、完全指定した場合を検証しています。
現状のコードでは、以下のような動作になります。
/articlesにアクセスするとArticlesコンポーネントのみを表示する/articles/listにアクセスするとArticleListコンポーネントのみを表示する/articles/editにアクセスするとArticleEditコンポーネントのみを表示する
各ページが完全に独立しており、ページ遷移のたびに全体が切り替わるようになります。
それぞれのコンポーネントを完全に別ページとして表示したい場合は、このパス指定でいいですが、「サイドバーを固定したまま、メインコンテンツだけ切り替えたい」というケースの実現はできないです。
◆ネストルーティングと< Outlet >を使った共通レイアウトの維持

src/components/Articles.jsx
import { Link, Outlet, useLocation } from 'react-router-dom'; export const Articles = () => { const location = useLocation(); // 現在のパスに応じてアクティブなリンクをハイライト const isActive = (path) => { return location.pathname === path; }; return ( <div className="flex gap-6"> {/* サイドバー(常に表示) */} <aside className="w-64 bg-white rounded-lg shadow p-4"> <h2 className="text-lg font-bold text-gray-900 mb-4">記事管理</h2> <nav className="space-y-2"> <Link to="/articles/list" className={`block px-4 py-2 rounded-md transition-colors ${ isActive('/articles/list') ? 'bg-blue-600 text-white' : 'text-gray-700 hover:bg-gray-100' }`} > 📄 記事一覧 </Link> <Link to="/articles/new" className={`block px-4 py-2 rounded-md transition-colors ${ isActive('/articles/new') ? 'bg-blue-600 text-white' : 'text-gray-700 hover:bg-gray-100' }`} > ✏️ 記事作成 </Link> </nav> <div className="mt-6 pt-6 border-t border-gray-200"> <h3 className="text-sm font-semibold text-gray-900 mb-2">統計情報</h3> <div className="space-y-2 text-sm text-gray-600"> <p> 総記事数: <span className="font-semibold text-gray-900">24</span> </p> <p> 公開中: <span className="font-semibold text-green-600">18</span> </p> <p> 下書き: <span className="font-semibold text-yellow-600">6</span> </p> </div> </div> </aside> {/* メインコンテンツ(ここに子ルートが表示される) */} <div className="flex-1 bg-white rounded-lg shadow p-6"> <Outlet /> </div> </div> ); };
src/components/ArticleList.jsx
export const ArticleList = () => { const articles = [ { id: 1, title: 'React Hooksの基礎', status: '公開', date: '2024-12-01' }, { id: 2, title: 'TypeScriptでの型定義', status: '下書き', date: '2024-12-02' }, { id: 3, title: 'Next.jsで始めるSSR', status: '公開', date: '2024-12-03' }, ]; return ( <div> <div className="flex justify-between items-center mb-6"> <h1 className="text-2xl font-bold text-gray-900">記事一覧</h1> </div> <div className="overflow-x-auto"> <table className="min-w-full divide-y divide-gray-200"> <thead className="bg-gray-50"> <tr> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> ID </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> タイトル </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> 公開日 </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> ステータス </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> 操作 </th> </tr> </thead> <tbody className="bg-white divide-y divide-gray-200"> {articles.map((article) => ( <tr key={article.id} className="hover:bg-gray-50 transition-colors"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{article.id}</td> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"> {article.title} </td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> {article.date} </td> <td className="px-6 py-4 whitespace-nowrap"> <span className={`px-2 py-1 text-xs font-medium rounded ${ article.status === '公開' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800' }`} > {article.status} </span> </td> <td className="px-6 py-4 whitespace-nowrap text-sm"> <button className="text-blue-600 hover:text-blue-700 mr-3">編集</button> <button className="text-red-600 hover:text-red-700">削除</button> </td> </tr> ))} </tbody> </table> </div> </div> ); };
src/components/ArticleNew.jsx
import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; export const ArticleNew = () => { const [title, setTitle] = useState(''); const [content, setContent] = useState(''); const [category, setCategory] = useState(''); const navigate = useNavigate(); const handleSubmit = (e) => { e.preventDefault(); // 保存処理 console.log('保存:', { title, content, category }); // 保存後、一覧ページに遷移 navigate('/articles/list'); }; return ( <div> <h1 className="text-2xl font-bold text-gray-900 mb-6">記事作成</h1> <form onSubmit={handleSubmit} className="space-y-6"> <div> <label className="block text-sm font-medium text-gray-700 mb-1">タイトル</label> <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} required className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="記事のタイトルを入力" /> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1">カテゴリ</label> <select value={category} onChange={(e) => setCategory(e.target.value)} required className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" > <option value="">選択してください</option> <option value="React">React</option> <option value="TypeScript">TypeScript</option> <option value="Next.js">Next.js</option> </select> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1">本文</label> <textarea value={content} onChange={(e) => setContent(e.target.value)} required rows={10} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="記事の本文を入力" /> </div> <div className="flex gap-3"> <button type="submit" className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors" > 保存 </button> <button type="button" onClick={() => navigate('/articles/list')} className="px-6 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors" > キャンセル </button> </div> </form> </div> ); };
src/App.jsx
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; import { Home } from './components/Home'; import { Articles } from './components/Articles'; import { ArticleList } from './components/ArticleList'; import { ArticleNew } from './components/ArticleNew'; import { Contact } from './components/Contact'; function App() { return ( <div className="min-h-screen bg-gray-50"> <BrowserRouter> <nav className="bg-white shadow-sm"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="flex justify-between h-16"> <div className="flex space-x-8"> <Link to="/" className="inline-flex items-center px-1 pt-1 text-gray-900 hover:text-blue-600 transition-colors" > ホーム </Link> <Link to="/articles" className="inline-flex items-center px-1 pt-1 text-gray-900 hover:text-blue-600 transition-colors" > 記事管理 </Link> <Link to="/contact" className="inline-flex items-center px-1 pt-1 text-gray-900 hover:text-blue-600 transition-colors" > お問い合わせ </Link> </div> </div> </div> </nav> <main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8"> <Routes> <Route path="/" element={<Home />} /> <Route path="/articles" element={<Articles />}> {/* Articlesの中にネストされたルート */} <Route path="list" element={<ArticleList />} /> <Route path="new" element={<ArticleNew />} /> </Route> <Route path="/contact" element={<Contact />} /> </Routes> </main> </BrowserRouter> </div> ); } export default App;
ネストルーティングと<Outlet>を使って、共通のサイドバーを維持したまま、メインコンテンツだけを切り替える実装をしてみます。
今回は、src/App.jsxで<Route path="/articles" element={<Articles />}>の中に<Route path="list" element={<ArticleList />} />や<Route path="new" element={<ArticleNew />} />をネストしたルーティングにしています。
例えば、/articles/listにアクセスすると、Articlesコンポーネントの中の<Outlet />に<ArticleList />コンポーネントが差し込まれてレンダリングされます。
このように、ネストルーティングと<Outlet>を使うことで、共通のサイドバーを維持したまま、メインコンテンツだけを切り替えることが可能になります。

ネストしたルーティングで注意が必要なのは、path="list"のように前方に/をつけないで相対パスで定義する必要があります。
もし前方に/をつけると絶対ルートパスとなります。
(<Route path="/list" element={<ArticleList />} />で/articles/list ではなく /list にマッチしてしまうことになります)
■ルーティング定義の分割
src/router/Router.jsx
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; import { Home } from '../components/Home'; import { Articles } from '../components/Articles'; import { Contact } from '../components/Contact'; import { articlesRoutes } from './articlesRoutes'; export const Router = () => { return ( <div className="min-h-screen bg-gray-50"> <BrowserRouter> <nav className="bg-white shadow-sm"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <div className="flex justify-between h-16"> <div className="flex space-x-8"> <Link to="/" className="inline-flex items-center px-1 pt-1 text-gray-900 hover:text-blue-600 transition-colors" > ホーム </Link> <Link to="/articles" className="inline-flex items-center px-1 pt-1 text-gray-900 hover:text-blue-600 transition-colors" > 記事管理 </Link> <Link to="/contact" className="inline-flex items-center px-1 pt-1 text-gray-900 hover:text-blue-600 transition-colors" > お問い合わせ </Link> </div> </div> </div> </nav> <main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8"> <Routes> <Route path="/" element={<Home />} /> <Route path="/articles" element={<Articles />}> {/* Articlesの中にネストされたルート */} {articlesRoutes.map(({ path, element }) => ( <Route key={path} path={path} element={element} /> ))} </Route> <Route path="/contact" element={<Contact />} /> </Routes> </main> </BrowserRouter> </div> ); };
src/router/articlesRoutes.js
import { ArticleList } from '../components/ArticleList'; import { ArticleNew } from '../components/ArticleNew'; export const articlesRoutes = [ { path: 'list', element: <ArticleList />, }, { path: 'new', element: <ArticleNew />, }, ];
src/App.jsx
import { Router } from './router/Router'; function App() { return <Router />; } export default App;
実務では、アプリケーションが大きくなり、ルーティング定義も大きくなってしまうことがあります。
そのような場合は、ルーティングを別ファイルに分割することができます。
例えば、src/router/articlesRoutes.jsに記事に関するルーティングだけを切り出して、それをsrc/router/Router.jsxでmapしてルーティング定義する事ができます。
このように関係のある機能でルーティングを分割することで、肥大化を防いで見通しを良くする事ができます。
■URLパラメータの活用

src/router/articlesRoutes.js
import { ArticleList } from '../components/ArticleList'; import { ArticleNew } from '../components/ArticleNew'; import { ArticleDetail } from '../components/ArticleDetail'; export const articlesRoutes = [ { path: 'list', element: <ArticleList />, }, { path: 'new', element: <ArticleNew />, }, { // :id は動的パラメータ(任意の値が入る) path: 'detail/:id', element: <ArticleDetail />, }, ];
src/data/articleData.js
export const articlesData = [ { id: 1, title: 'React Hooksの基礎', content: `React Hooksについての詳細な解説がここに入ります... さらに内容が続きます...`, category: 'React', status: '公開', date: '2024-12-01', }, { id: 2, title: 'TypeScriptでの型定義', content: `TypeScriptでの型定義についての詳細な解説がここに入ります... さらに内容が続きます..`, category: 'TypeScript', status: '下書き', date: '2024-12-02', }, { id: 3, title: 'Next.jsで始めるSSR', content: `Next.jsでのサーバーサイドレンダリングについての詳細な解説がここに入ります...`, category: 'Next.js', status: '公開', date: '2024-12-03', }, ];
src/components/ArticleList.jsx
import { articlesData } from '../data/articleData'; import { Link } from 'react-router-dom'; export const ArticleList = () => { return ( <div> <div className="flex justify-between items-center mb-6"> <h1 className="text-2xl font-bold text-gray-900">記事一覧</h1> </div> <div className="overflow-x-auto"> <table className="min-w-full divide-y divide-gray-200"> <thead className="bg-gray-50"> <tr> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> ID </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> タイトル </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> 公開日 </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> ステータス </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> 操作 </th> </tr> </thead> <tbody className="bg-white divide-y divide-gray-200"> {articlesData.map((article) => ( <tr key={article.id} className="hover:bg-gray-50 transition-colors"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{article.id}</td> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium"> {/* 動的にidを含めたリンク */} <Link to={`/articles/detail/${article.id}`} className="text-gray-900 hover:text-slate-700 hover:underline underline-offset-4 transition-colors" > {article.title} </Link> </td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> {article.date} </td> <td className="px-6 py-4 whitespace-nowrap"> <span className={`px-2 py-1 text-xs font-medium rounded ${ article.status === '公開' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800' }`} > {article.status} </span> </td> <td className="px-6 py-4 whitespace-nowrap text-sm"> <button className="text-blue-600 hover:text-blue-700 mr-3">編集</button> <button className="text-red-600 hover:text-red-700">削除</button> </td> </tr> ))} </tbody> </table> </div> </div> ); };
src/components/ArticleDetail.jsx
import { useParams, Link } from 'react-router-dom'; import { articlesData } from '../data/articleData'; export const ArticleDetail = () => { const { id } = useParams(); const article = articlesData.find((item) => String(item.id) === id); return ( <div> <Link to="/articles/list" className="inline-flex items-center text-blue-600 hover:text-blue-700 mb-4" > ← 一覧に戻る </Link> <article className="prose max-w-none"> <div className="mb-4"> <span className="px-3 py-1 text-sm font-medium bg-blue-100 text-blue-800 rounded"> {article.category} </span> </div> <h1 className="text-3xl font-bold text-gray-900 mb-2">{article.title}</h1> <p className="text-gray-500 mb-6">{article.date}</p> <div className="text-gray-700 leading-relaxed whitespace-pre-wrap">{article.content}</div> </article> </div> ); };
ブログの一覧ページから詳細ページへ遷移するときに、URLに動的な値を含めたい場合は:idのような記法を使います。
src/router/articlesRoutes.jsxでpath: 'detail/:id'と書いて、idが動的に入ることを許容したルーティングを定義します。
src/components/ArticleList.jsxで<Link to={`/articles/detail/${article.id}`} />として、動的なidを含めたaタグを生成しています。
詳細ページに遷移した際は、useParams()フックを使うことで、URLの:id部分を簡単に取得できます。
■クエリパラメータの活用

src/components/ArticleList.jsx
import { articlesData } from '../data/articleData'; import { Link, useSearchParams } from 'react-router-dom'; import { ArticleSearch } from './ArticleSearch'; export const ArticleList = () => { // useSearchParams: クエリパラメータを読み書きするためのフック const [searchParams] = useSearchParams(); // クエリパラメータから値を取得 const categoryFilter = searchParams.get('category') || ''; const searchQuery = searchParams.get('search') || ''; const statusFilter = searchParams.get('status') || ''; // カテゴリ一覧を取得 const categories = [...new Set(articlesData.map((article) => article.category))]; // フィルタリングされた記事一覧 const filteredArticles = articlesData.filter((article) => { const matchesCategory = categoryFilter === '' || article.category === categoryFilter; const matchesSearch = searchQuery === '' || article.title.toLowerCase().includes(searchQuery.toLowerCase()); const matchesStatus = statusFilter === '' || article.status === statusFilter; return matchesCategory && matchesSearch && matchesStatus; }); return ( <div> <div className="flex justify-between items-center mb-6"> <h1 className="text-2xl font-bold text-gray-900">記事一覧</h1> </div> {/* 検索コンポーネント */} <ArticleSearch categories={categories} /> <div className="overflow-x-auto"> <p className="text-sm text-gray-600 mb-2">検索結果: {filteredArticles.length}件</p> <table className="min-w-full divide-y divide-gray-200"> <thead className="bg-gray-50"> <tr> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> ID </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> タイトル </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> カテゴリ </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> 公開日 </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> ステータス </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> 操作 </th> </tr> </thead> <tbody className="bg-white divide-y divide-gray-200"> {filteredArticles.map((article) => ( <tr key={article.id} className="hover:bg-gray-50 transition-colors"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{article.id}</td> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium"> <Link to={`/articles/detail/${article.id}`} className="text-gray-900 hover:text-slate-700 hover:underline underline-offset-4 transition-colors" > {article.title} </Link> </td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> {article.category} </td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> {article.date} </td> <td className="px-6 py-4 whitespace-nowrap"> <span className={`px-2 py-1 text-xs font-medium rounded ${ article.status === '公開' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800' }`} > {article.status} </span> </td> <td className="px-6 py-4 whitespace-nowrap text-sm"> <button className="text-blue-600 hover:text-blue-700 mr-3">編集</button> <button className="text-red-600 hover:text-red-700">削除</button> </td> </tr> ))} </tbody> </table> </div> </div> ); };
src/components/ArticleSearch.jsx
import { useSearchParams } from 'react-router-dom'; export const ArticleSearch = ({ categories }) => { // useSearchParams: クエリパラメータを読み書きするためのフック // URLの ?category=React&search=hooks のような部分を扱う const [searchParams, setSearchParams] = useSearchParams(); // クエリパラメータから値を取得 const categoryFilter = searchParams.get('category') || ''; const searchQuery = searchParams.get('search') || ''; const statusFilter = searchParams.get('status') || ''; // フィルター変更時のハンドラー const handleFilterChange = (key, value) => { const newParams = new URLSearchParams(searchParams); if (value) { newParams.set(key, value); } else { newParams.delete(key); } setSearchParams(newParams); }; // フィルターをクリア const clearFilters = () => { setSearchParams(new URLSearchParams()); }; return ( <div className="bg-white rounded-lg shadow p-4 mb-6"> <h2 className="text-sm font-semibold text-gray-700 mb-3">🔍 検索</h2> <div className="flex flex-wrap gap-4 items-end"> {/* 検索入力 */} <div className="flex-1 min-w-48"> <label className="block text-xs text-gray-500 mb-1">タイトル検索</label> <input type="text" value={searchQuery} onChange={(e) => handleFilterChange('search', e.target.value)} placeholder="検索..." className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div> {/* カテゴリ選択 */} <div> <label className="block text-xs text-gray-500 mb-1">カテゴリ</label> <select value={categoryFilter} onChange={(e) => handleFilterChange('category', e.target.value)} className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" > <option value="">すべて</option> {categories.map((cat) => ( <option key={cat} value={cat}> {cat} </option> ))} </select> </div> {/* ステータス選択 */} <div> <label className="block text-xs text-gray-500 mb-1">ステータス</label> <select value={statusFilter} onChange={(e) => handleFilterChange('status', e.target.value)} className="px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" > <option value="">すべて</option> <option value="公開">公開</option> <option value="下書き">下書き</option> </select> </div> {/* クリアボタン */} <button onClick={clearFilters} className="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-md transition-colors" > クリア </button> </div> </div> ); };
クエリパラメータは、検索機能やフィルタリング機能でよく使います。
利用用途としては、URLの?keyword=react&category=frontendのような形式から?以降の値を取得して、絞り込むなどです。
?category=React&search=hooks のような部分を扱うにはuseSearchParamsを使います。
React RouterのuseSearchParamsは、ブラウザのURLバーにあるクエリパラメータ(?以降)を読み書きするフックになります。
内部でURLSearchParamsインスタンスを作成して、その更新用関数と一緒に返してくれます。
setSearchParamsに検索条件を持ったURLSearchParamsインスタンスを渡すことで、URLの?keyword=react&category=frontendのような形式を作成してくれます。
useSearchParamsの良いところは、同じURL(同じページ)にいると、同じクエリパラメーターを参照することです。
そのため、ArticleSearch.jsxでuseSearchParamsを使ってクエリパラメーターを作成すると、親コンポーネントのArticleList.jsxも同じURLなので、同じクエリパラメーターを参照してくれます。
useSearchParams | React Router
■stateを使ったページ間のデータ受け渡し

src/components/ArticleList.jsx
import { articlesData } from '../data/articleData'; import { Link, useSearchParams } from 'react-router-dom'; import { ArticleSearch } from './ArticleSearch'; export const ArticleList = () => { const [searchParams] = useSearchParams(); const categoryFilter = searchParams.get('category') || ''; const searchQuery = searchParams.get('search') || ''; const statusFilter = searchParams.get('status') || ''; const categories = [...new Set(articlesData.map((article) => article.category))]; const filteredArticles = articlesData.filter((article) => { const matchesCategory = categoryFilter === '' || article.category === categoryFilter; const matchesSearch = searchQuery === '' || article.title.toLowerCase().includes(searchQuery.toLowerCase()); const matchesStatus = statusFilter === '' || article.status === statusFilter; return matchesCategory && matchesSearch && matchesStatus; }); return ( <div> <div className="flex justify-between items-center mb-6"> <h1 className="text-2xl font-bold text-gray-900">記事一覧</h1> </div> <ArticleSearch categories={categories} /> <div className="overflow-x-auto"> <p className="text-sm text-gray-600 mb-2">検索結果: {filteredArticles.length}件</p> <table className="min-w-full divide-y divide-gray-200"> <thead className="bg-gray-50"> <tr> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> ID </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> タイトル </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> カテゴリ </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> 公開日 </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> ステータス </th> <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> 操作 </th> </tr> </thead> <tbody className="bg-white divide-y divide-gray-200"> {filteredArticles.map((article) => ( <tr key={article.id} className="hover:bg-gray-50 transition-colors"> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{article.id}</td> <td className="px-6 py-4 whitespace-nowrap text-sm font-medium"> <Link to={`/articles/detail/${article.id}`} state={article} // 記事データをstateとして渡す className="text-gray-900 hover:text-slate-700 hover:underline underline-offset-4 transition-colors" > {article.title} </Link> </td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> {article.category} </td> <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500"> {article.date} </td> <td className="px-6 py-4 whitespace-nowrap"> <span className={`px-2 py-1 text-xs font-medium rounded ${ article.status === '公開' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800' }`} > {article.status} </span> </td> <td className="px-6 py-4 whitespace-nowrap text-sm"> <button className="text-blue-600 hover:text-blue-700 mr-3">編集</button> <button className="text-red-600 hover:text-red-700">削除</button> </td> </tr> ))} </tbody> </table> </div> </div> ); };
src/components/ArticleDetail.jsx
import { useLocation, Link } from 'react-router-dom'; export const ArticleDetail = () => { // 記事のデータはLinkのstate経由で受け取る const { state } = useLocation(); return ( <div> <Link to="/articles/list" className="inline-flex items-center text-blue-600 hover:text-blue-700 mb-4" > ← 一覧に戻る </Link> <article className="prose max-w-none"> <div className="mb-4"> <span className="px-3 py-1 text-sm font-medium bg-blue-100 text-blue-800 rounded"> {state.category} </span> </div> <h1 className="text-3xl font-bold text-gray-900 mb-2">{state.title}</h1> <p className="text-gray-500 mb-6">{state.date}</p> <div className="text-gray-700 leading-relaxed whitespace-pre-wrap">{state.content}</div> </article> </div> ); };
一覧ページから詳細ページに遷移する際、APIを再度呼ばずにデータを渡したい場合や、URLにidを含めて、すべての記事から再度検索させるということをしないで、詳細ページにデータを渡すことができます。
そんなときはstateを使います。
ArticleList.jsxでstate={article}と書くことで、詳細ページに遷移する際に、stateとして記事データを渡すことができます。
詳細ページでは、const { state } = useLocation();とすることで、一覧ページから渡した記事のデータを取得することができます。
■JavaScriptでのページ遷移

src/data/articleData.js
export const articlesData = [ { id: Math.floor(Math.random() * 1000), title: 'React Hooksの基礎', content: `React Hooksについての詳細な解説がここに入ります... さらに内容が続きます...`, category: 'React', status: '公開', date: '2024-12-01', }, { id: Math.floor(Math.random() * 1000), title: 'TypeScriptでの型定義', content: `TypeScriptでの型定義についての詳細な解説がここに入ります... さらに内容が続きます..`, category: 'TypeScript', status: '下書き', date: '2024-12-02', }, { id: Math.floor(Math.random() * 1000), title: 'Next.jsで始めるSSR', content: `Next.jsでのサーバーサイドレンダリングについての詳細な解説がここに入ります...`, category: 'Next.js', status: '公開', date: '2024-12-03', }, ];
src/components/ArticleNew.jsx
import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { articlesData } from '../data/articleData'; export const ArticleNew = () => { const [title, setTitle] = useState(''); const [content, setContent] = useState(''); const [category, setCategory] = useState(''); const [status, setStatus] = useState('下書き'); const [date, setDate] = useState(''); const navigate = useNavigate(); const handleSubmit = (e) => { e.preventDefault(); // 保存処理 articlesData.push({ id: Math.floor(Math.random() * 1000), title, content, category, status, date, }); // 保存後、一覧ページに遷移 navigate('/articles/list'); }; return ( <div> <h1 className="text-2xl font-bold text-gray-900 mb-6">記事作成</h1> <form onSubmit={handleSubmit} className="space-y-6"> <div> <label className="block text-sm font-medium text-gray-700 mb-1">タイトル</label> <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} required className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="記事のタイトルを入力" /> </div> <div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div> <label className="block text-sm font-medium text-gray-700 mb-1">カテゴリ</label> <select value={category} onChange={(e) => setCategory(e.target.value)} required className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" > <option value="">選択してください</option> <option value="React">React</option> <option value="TypeScript">TypeScript</option> <option value="Next.js">Next.js</option> </select> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1">ステータス</label> <select value={status} onChange={(e) => setStatus(e.target.value)} required className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" > <option value="下書き">下書き</option> <option value="公開">公開</option> </select> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1">公開日</label> <input type="date" value={date} onChange={(e) => setDate(e.target.value)} required className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div> </div> <div> <label className="block text-sm font-medium text-gray-700 mb-1">本文</label> <textarea value={content} onChange={(e) => setContent(e.target.value)} required rows={10} className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="記事の本文を入力" /> </div> <div className="flex gap-3"> <button type="submit" className="px-6 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors" > 保存 </button> <button type="button" onClick={() => navigate('/articles/list')} className="px-6 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition-colors" > キャンセル </button> </div> </form> </div> ); };
記事を新規投稿した後に、JavaScript側でページ遷移を制御したい場合や、ボタンクリック時にページを遷移したい場合があります。
そのような動作を実現したいときは、useNavigateを使います。
ArticleNew.jsxのhandleSubmit関数の中で保存処理をした後に、navigate('/articles/list');でURLを一覧ページのURLに変えて、再レンダリングさせることでページ遷移を実現しています。
よく使うパターンとして、以下のような物があります。
navigate('/path')- 指定したパスに遷移navigate(-1)- 前のページに戻るnavigate(1)- 次のページに進むnavigate('/path', { replace: true })- 履歴を置き換えて遷移(戻るボタンで戻れなくする)
■まとめ
公式を見ると、React Routerだけでもたくさんの機能があるので、全部学ぶのは大変ですが、今回ピックアップした機能だけでも大半のことは実装できるのではないかなーと思っています。
フロントをメインにやられている方は、バックエンド側も学ばないとなーと感じることはあると思いますが、React + React RouterでのSPA開発ができれば、バックエンド側の基本的な機能にSupabaseを導入して「認証」や「データ保持」もできるので、フルスタックに開発できるなーと感じています。
2026年は、Supabaseを導入して「認証」や「データ保持」も組み込んでみたいなーと思っています。
■最後に
ここまで記事を読んでいただきありがとうございました!
弊社ではエンジニアを募集しております。
ご興味がありましたらカジュアル面談も可能ですので、下記リンクより是非ご応募ください!
iimon採用サイト / Wantedly
明日のアドベントカレンダー担当は「mariさん」です!
新卒入社からどんどん成長されている「mariさん」がどんな記事を書いてくれるか楽しみですね!!