React画面遷移の基本|初心者でも迷わない実装方法まとめ
React初心者向けの画面遷移完全ガイド。React Routerの基本から動的ルーティング、パラメータ受け渡しまで実例で詳しく解説
みなさん、Reactで「画面を切り替えたいけど、どうすればいいの?」と悩んでいませんか?
「React Routerって何だか難しそう...」 「パラメータの受け渡しってどうやるの?」 「エラーが出てページが表示されない!」
こんな疑問を抱えている方も多いでしょう。
この記事では、React初心者が迷わずに画面遷移を実装できるよう、基本的な概念から実践的な方法まで分かりやすく解説します。 実際のコード例も豊富に紹介するので、手を動かしながら学習できますよ。
React画面遷移の基本的な仕組み
まず、「そもそもReactってどうやって画面を切り替えるの?」という疑問から解決していきましょう。
SPAって何?基本を理解しよう
ReactではSPA(シングルページアプリケーション)という方式でWebサイトを作ります。
SPAの特徴を簡単に説明すると
- 実際のHTMLファイルは1つだけ
- JavaScriptでコンテンツを動的に変更
- ページ全体をリロードしない
- 高速で滑らかな画面切り替え
従来のWebサイトとSPAの違いを見てみましょう。
// 従来のWebサイト(マルチページ)
// page1.html → page2.html → page3.html
// (ページ全体がリロードされる)
// SPAの場合(React)
// index.html 内で <Component1> → <Component2> → <Component3>
// (コンテンツのみ切り替わる)
function App() {
const [currentPage, setCurrentPage] = useState('home');
const renderPage = () => {
switch (currentPage) {
case 'home':
return <HomePage />;
case 'about':
return <AboutPage />;
case 'contact':
return <ContactPage />;
default:
return <HomePage />;
}
};
return (
<div className="app">
<nav>
<button onClick={() => setCurrentPage('home')}>ホーム</button>
<button onClick={() => setCurrentPage('about')}>会社概要</button>
<button onClick={() => setCurrentPage('contact')}>お問い合わせ</button>
</nav>
<main>
{renderPage()}
</main>
</div>
);
}
このコードを詳しく見ていきますね。
まず、現在表示するページを管理する状態を作ります。
const [currentPage, setCurrentPage] = useState('home');
useState
で現在のページを記録しています。
最初は'home'
が表示されるように設定しています。
次に、状態に応じてコンポーネントを切り替える関数です。
const renderPage = () => {
switch (currentPage) {
case 'home':
return <HomePage />;
case 'about':
return <AboutPage />;
// ...
}
};
switch
文で現在の状態をチェックして、対応するコンポーネントを返します。
ボタンクリックで状態を変更する部分はこちらです。
<button onClick={() => setCurrentPage('home')}>ホーム</button>
ボタンをクリックするとsetCurrentPage
で状態が変わり、画面が切り替わります。
でも、この方法だとURLが変わらないという問題があります。 そこで登場するのがReact Routerです。
React Routerの役割
React Routerは、ReactアプリでURLと画面を連動させるライブラリです。
React Routerを使う理由
- URLが変わるので、ブックマークが可能
- ブラウザの戻る・進むボタンが正常に動作
- 特定のページに直接アクセスできる
- SEO(検索エンジン最適化)に有利
簡単に言うと、普通のWebサイトと同じように使えるということですね。
React Routerの導入と基本設定
それでは、実際にReact Routerを使って画面遷移を作ってみましょう。
インストール
まずはReact Routerをプロジェクトにインストールします。
# React Router をインストール
npm install react-router-dom
# または yarn を使用する場合
yarn add react-router-dom
このコマンドを実行すると、React Routerが使えるようになります。
基本的な設定方法
App.jsファイルにルーターの設定を書いてみましょう。
// App.js - ルーターの基本設定
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
function App() {
return (
<Router>
<div className="app">
{/* ナビゲーション */}
<nav className="navigation">
<Link to="/" className="nav-link">ホーム</Link>
<Link to="/about" className="nav-link">会社概要</Link>
<Link to="/products" className="nav-link">商品一覧</Link>
<Link to="/contact" className="nav-link">お問い合わせ</Link>
</nav>
{/* ルート定義 */}
<main className="main-content">
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/products" element={<ProductsPage />} />
<Route path="/contact" element={<ContactPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</main>
</div>
</Router>
);
}
export default App;
このコードの重要な部分を順番に説明しますね。
まず、必要なコンポーネントをインポートします。
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
BrowserRouter
: ルーター機能の土台Routes
: ルートをまとめる容器Route
: 個別のルート定義Link
: ページ遷移用のリンク
次に、全体をRouter
で囲みます。
<Router>
{/* この中でルーティングが使える */}
</Router>
ナビゲーション部分ではLink
を使います。
<Link to="/" className="nav-link">ホーム</Link>
<Link to="/about" className="nav-link">会社概要</Link>
to
プロパティで遷移先のURLを指定します。
<a>
タグの代わりにLink
を使うのがポイントです。
ルート定義では、URLとコンポーネントを関連付けます。
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
path
でURL、element
で表示するコンポーネントを指定します。
path="*"
は、どのルートにも一致しなかった場合の404ページです。
各ページコンポーネントの作成
次に、実際に表示するページコンポーネントを作ってみましょう。
// pages/HomePage.js
function HomePage() {
return (
<div className="home-page">
<h1>ホームページへようこそ</h1>
<p>私たちのサービスをご紹介します。</p>
<section className="features">
<h2>主な特徴</h2>
<div className="feature-list">
<div className="feature-item">
<h3>高品質</h3>
<p>厳選された素材を使用</p>
</div>
<div className="feature-item">
<h3>迅速対応</h3>
<p>24時間以内にお返事</p>
</div>
<div className="feature-item">
<h3>安心価格</h3>
<p>業界最安値を実現</p>
</div>
</div>
</section>
</div>
);
}
このように、普通のReactコンポーネントと同じ書き方でページを作れます。 特別な設定は必要ありません。
// pages/AboutPage.js
function AboutPage() {
return (
<div className="about-page">
<h1>会社概要</h1>
<section className="company-info">
<h2>企業情報</h2>
<table>
<tbody>
<tr>
<th>会社名</th>
<td>株式会社サンプル</td>
</tr>
<tr>
<th>設立</th>
<td>2020年4月1日</td>
</tr>
<tr>
<th>代表者</th>
<td>代表取締役 山田太郎</td>
</tr>
<tr>
<th>所在地</th>
<td>東京都渋谷区sample 1-2-3</td>
</tr>
</tbody>
</table>
</section>
<section className="mission">
<h2>企業理念</h2>
<p>
私たちは、技術革新を通じて社会課題の解決に貢献し、
すべての人が豊かな生活を送れる世界の実現を目指します。
</p>
</section>
</div>
);
}
商品一覧ページでは、データの取得も含めてみましょう。
// pages/ProductsPage.js
import { useState, useEffect } from 'react';
function ProductsPage() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchProducts();
}, []);
const fetchProducts = async () => {
try {
// APIから商品データを取得(今回はダミーデータ)
const sampleProducts = [
{ id: 1, name: "商品A", price: 1000, image: "/images/product1.jpg" },
{ id: 2, name: "商品B", price: 1500, image: "/images/product2.jpg" },
{ id: 3, name: "商品C", price: 2000, image: "/images/product3.jpg" },
];
setProducts(sampleProducts);
setLoading(false);
} catch (error) {
console.error('商品取得エラー:', error);
setLoading(false);
}
};
if (loading) {
return <div className="loading">商品を読み込み中...</div>;
}
return (
<div className="products-page">
<h1>商品一覧</h1>
<div className="products-grid">
{products.map(product => (
<div key={product.id} className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p className="price">¥{product.price.toLocaleString()}</p>
<button className="buy-button">購入する</button>
</div>
))}
</div>
</div>
);
}
お問い合わせページではフォームも作ってみましょう。
// pages/ContactPage.js
import { useState } from 'react';
function ContactPage() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('送信データ:', formData);
alert('お問い合わせを送信しました!');
// フォームをリセット
setFormData({ name: '', email: '', message: '' });
};
return (
<div className="contact-page">
<h1>お問い合わせ</h1>
<form onSubmit={handleSubmit} className="contact-form">
<div className="form-group">
<label htmlFor="name">お名前</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleInputChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="email">メールアドレス</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="message">メッセージ</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleInputChange}
rows="5"
required
/>
</div>
<button type="submit" className="submit-button">
送信する
</button>
</form>
</div>
);
}
404ページも作っておきましょう。
// pages/NotFoundPage.js
import { Link } from 'react-router-dom';
function NotFoundPage() {
return (
<div className="not-found-page">
<h1>404 - ページが見つかりません</h1>
<p>お探しのページは存在しないか、移動された可能性があります。</p>
<Link to="/" className="home-link">ホームに戻る</Link>
</div>
);
}
これで基本的な画面遷移の完成です!
動的ルーティングでもっと便利に
URLに変数を含む動的ルーティングを使って、より柔軟な画面遷移を実装してみましょう。
パラメータ付きルートの設定
例えば、商品詳細ページを作る場合を考えてみましょう。
// App.js - 動的ルートの追加
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
function App() {
return (
<Router>
<div className="app">
<nav className="navigation">
<Link to="/">ホーム</Link>
<Link to="/products">商品一覧</Link>
<Link to="/users">ユーザー一覧</Link>
</nav>
<main>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/products" element={<ProductsPage />} />
<Route path="/products/:id" element={<ProductDetailPage />} />
<Route path="/users" element={<UsersPage />} />
<Route path="/users/:userId" element={<UserProfilePage />} />
<Route path="/users/:userId/posts" element={<UserPostsPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</main>
</div>
</Router>
);
}
ここでポイントとなるのは、:id
や:userId
の部分です。
<Route path="/products/:id" element={<ProductDetailPage />} />
:id
は「パラメータ」と呼ばれ、URLの一部を変数として受け取れます。
例えば、/products/1
、/products/2
、/products/abc
などにマッチします。
パラメータの取得と活用
実際にパラメータを取得して使ってみましょう。
// pages/ProductDetailPage.js
import { useState, useEffect } from 'react';
import { useParams, Link, useNavigate } from 'react-router-dom';
function ProductDetailPage() {
const { id } = useParams(); // URLパラメータを取得
const navigate = useNavigate(); // プログラムで画面遷移
const [product, setProduct] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchProduct();
}, [id]);
const fetchProduct = async () => {
try {
setLoading(true);
setError(null);
// ダミーデータ(実際はAPIから取得)
const sampleProducts = {
'1': {
id: 1,
name: "高級腕時計",
price: 50000,
description: "精密な機械式腕時計です。職人が一つ一つ手作業で仕上げています。",
images: ["/images/watch1.jpg", "/images/watch2.jpg"],
category: "アクセサリー",
inStock: true,
specifications: {
"ムーブメント": "機械式",
"ケース素材": "ステンレススチール",
"防水性": "10気圧防水",
"保証期間": "2年間"
}
},
'2': {
id: 2,
name: "レザーバッグ",
price: 25000,
description: "上質な本革を使用したビジネスバッグです。",
images: ["/images/bag1.jpg"],
category: "バッグ",
inStock: false,
specifications: {
"素材": "本革(牛革)",
"サイズ": "W40×H30×D10cm",
"重量": "1.2kg",
"カラー": "ブラック、ブラウン"
}
}
};
const productData = sampleProducts[id];
if (!productData) {
throw new Error('商品が見つかりません');
}
setProduct(productData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const handleAddToCart = () => {
if (!product.inStock) {
alert('申し訳ございません。この商品は現在在庫切れです。');
return;
}
alert(`${product.name} をカートに追加しました!`);
};
const handleBuyNow = () => {
if (!product.inStock) {
alert('申し訳ございません。この商品は現在在庫切れです。');
return;
}
// 購入ページに遷移
navigate(`/purchase/${product.id}`);
};
if (loading) {
return <div className="loading">商品情報を読み込み中...</div>;
}
if (error) {
return (
<div className="error-page">
<h2>エラーが発生しました</h2>
<p>{error}</p>
<Link to="/products">商品一覧に戻る</Link>
</div>
);
}
return (
<div className="product-detail-page">
<nav className="breadcrumb">
<Link to="/">ホーム</Link> >
<Link to="/products">商品一覧</Link> >
<span>{product.name}</span>
</nav>
<div className="product-detail">
<div className="product-images">
<div className="main-image">
<img src={product.images[0]} alt={product.name} />
</div>
{product.images.length > 1 && (
<div className="thumbnail-images">
{product.images.map((image, index) => (
<img key={index} src={image} alt={`${product.name} ${index + 1}`} />
))}
</div>
)}
</div>
<div className="product-info">
<h1>{product.name}</h1>
<p className="category">{product.category}</p>
<p className="price">¥{product.price.toLocaleString()}</p>
<div className="stock-status">
{product.inStock ? (
<span className="in-stock">在庫あり</span>
) : (
<span className="out-of-stock">在庫切れ</span>
)}
</div>
<p className="description">{product.description}</p>
<div className="product-actions">
<button
onClick={handleAddToCart}
className={`add-to-cart ${!product.inStock ? 'disabled' : ''}`}
disabled={!product.inStock}
>
カートに追加
</button>
<button
onClick={handleBuyNow}
className={`buy-now ${!product.inStock ? 'disabled' : ''}`}
disabled={!product.inStock}
>
今すぐ購入
</button>
</div>
<div className="specifications">
<h3>商品仕様</h3>
<table>
<tbody>
{Object.entries(product.specifications).map(([key, value]) => (
<tr key={key}>
<th>{key}</th>
<td>{value}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
}
export default ProductDetailPage;
このコードの重要な部分を詳しく見ていきましょう。
まず、パラメータの取得部分です。
const { id } = useParams(); // URLパラメータを取得
useParams
フックを使うと、URLの:id
部分を取得できます。
例えば/products/1
にアクセスすると、id
は"1"
になります。
プログラムで画面遷移する部分はこちらです。
const navigate = useNavigate(); // プログラムで画面遷移
const handleBuyNow = () => {
navigate(`/purchase/${product.id}`);
};
useNavigate
フックを使うと、ボタンクリックなどで画面遷移できます。
データ取得の部分では、パラメータが変わったときに再取得します。
useEffect(() => {
fetchProduct();
}, [id]); // idが変わったときに実行
依存配列に[id]
を指定することで、URLのIDが変わるたびに商品データを取得し直します。
複数パラメータの処理
ユーザープロフィールページのように、複数のパラメータを使う場合もあります。
// pages/UserProfilePage.js
import { useState, useEffect } from 'react';
import { useParams, Link, Outlet } from 'react-router-dom';
function UserProfilePage() {
const { userId } = useParams();
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUser();
}, [userId]);
const fetchUser = async () => {
try {
// ダミーユーザーデータ
const sampleUsers = {
'1': {
id: 1,
name: "田中太郎",
email: "tanaka@example.com",
avatar: "/images/avatar1.jpg",
bio: "フロントエンド開発者です。ReactとVue.jsが得意です。",
joinDate: "2023-01-15",
postsCount: 24,
followersCount: 120,
followingCount: 80
},
'2': {
id: 2,
name: "佐藤花子",
email: "sato@example.com",
avatar: "/images/avatar2.jpg",
bio: "UXデザイナーとして働いています。ユーザビリティの向上に取り組んでいます。",
joinDate: "2022-11-03",
postsCount: 36,
followersCount: 200,
followingCount: 150
}
};
const userData = sampleUsers[userId];
setUser(userData);
setLoading(false);
} catch (error) {
console.error('ユーザー取得エラー:', error);
setLoading(false);
}
};
if (loading) {
return <div className="loading">ユーザー情報を読み込み中...</div>;
}
if (!user) {
return (
<div className="error-page">
<h2>ユーザーが見つかりません</h2>
<Link to="/users">ユーザー一覧に戻る</Link>
</div>
);
}
return (
<div className="user-profile-page">
<div className="user-header">
<div className="user-avatar">
<img src={user.avatar} alt={user.name} />
</div>
<div className="user-info">
<h1>{user.name}</h1>
<p className="user-email">{user.email}</p>
<p className="user-bio">{user.bio}</p>
<p className="join-date">参加日: {user.joinDate}</p>
</div>
</div>
<div className="user-stats">
<div className="stat-item">
<strong>{user.postsCount}</strong>
<span>投稿</span>
</div>
<div className="stat-item">
<strong>{user.followersCount}</strong>
<span>フォロワー</span>
</div>
<div className="stat-item">
<strong>{user.followingCount}</strong>
<span>フォロー中</span>
</div>
</div>
<nav className="user-navigation">
<Link to={`/users/${userId}`} className="nav-tab">
プロフィール
</Link>
<Link to={`/users/${userId}/posts`} className="nav-tab">
投稿一覧
</Link>
</nav>
<div className="user-content">
<Outlet /> {/* 子ルートのコンテンツが表示される */}
</div>
</div>
);
}
Outlet
コンポーネントは、子ルートの内容を表示する場所を指定します。
ユーザーの投稿一覧ページも作ってみましょう。
// pages/UserPostsPage.js
function UserPostsPage() {
const { userId } = useParams();
const [posts, setPosts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchUserPosts();
}, [userId]);
const fetchUserPosts = async () => {
try {
// ダミー投稿データ
const samplePosts = [
{
id: 1,
title: "Reactの状態管理について",
content: "Reactで状態管理を行う際のベストプラクティスを紹介します...",
publishedAt: "2024-01-15",
likesCount: 25,
commentsCount: 8
},
{
id: 2,
title: "CSS Grid レイアウトの基本",
content: "CSS Gridを使った柔軟なレイアウト設計について...",
publishedAt: "2024-01-10",
likesCount: 18,
commentsCount: 5
}
];
setPosts(samplePosts);
setLoading(false);
} catch (error) {
console.error('投稿取得エラー:', error);
setLoading(false);
}
};
if (loading) {
return <div className="loading">投稿を読み込み中...</div>;
}
return (
<div className="user-posts">
<h2>投稿一覧</h2>
{posts.length === 0 ? (
<p>まだ投稿がありません。</p>
) : (
<div className="posts-list">
{posts.map(post => (
<article key={post.id} className="post-card">
<h3>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</h3>
<p className="post-excerpt">{post.content.substring(0, 100)}...</p>
<div className="post-meta">
<time>{post.publishedAt}</time>
<span>❤️ {post.likesCount}</span>
<span>💬 {post.commentsCount}</span>
</div>
</article>
))}
</div>
)}
</div>
);
}
このように、パラメータを使うことで柔軟な画面遷移が実現できます。
プログラムで画面遷移をコントロール
リンクをクリックする以外にも、ボタンクリックやフォーム送信時に画面遷移したい場合がありますよね。
useNavigateフックの基本的な使い方
useNavigate
フックを使って、プログラムで画面遷移を実行してみましょう。
// components/LoginForm.js
import { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
const [formData, setFormData] = useState({
email: '',
password: ''
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e) => {
e.preventDefault();
setLoading(true);
setError('');
try {
// ログインAPI呼び出し(今回はダミー)
if (formData.email === 'test@example.com' && formData.password === 'password') {
// ローカルストレージにトークンを保存
localStorage.setItem('authToken', 'dummy-token');
// ダッシュボードページに遷移
navigate('/dashboard', { replace: true });
} else {
throw new Error('メールアドレスまたはパスワードが間違っています');
}
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
const handleGoogleLogin = () => {
// Google OAuth ログイン(ダミー)
alert('Googleログインは実装中です');
};
const handleForgotPassword = () => {
// パスワードリセットページに遷移
navigate('/forgot-password');
};
return (
<div className="login-form">
<h2>ログイン</h2>
{error && (
<div className="error-message">
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="email">メールアドレス</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
required
/>
</div>
<div className="form-group">
<label htmlFor="password">パスワード</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleInputChange}
required
/>
</div>
<button type="submit" disabled={loading} className="login-button">
{loading ? 'ログイン中...' : 'ログイン'}
</button>
</form>
<div className="login-options">
<button onClick={handleGoogleLogin} className="google-login">
Googleでログイン
</button>
<button onClick={handleForgotPassword} className="forgot-password">
パスワードを忘れた方はこちら
</button>
</div>
<div className="signup-link">
<p>
アカウントをお持ちでない方は
<Link to="/signup">新規登録</Link>
</p>
</div>
</div>
);
}
export default LoginForm;
ここでの重要なポイントを詳しく見てみましょう。
useNavigate
フックの使い方です。
const navigate = useNavigate();
// 基本的な遷移
navigate('/dashboard');
// 履歴を置き換える(戻るボタンで元のページに戻れない)
navigate('/dashboard', { replace: true });
// 相対パスでの遷移
navigate('../other-page');
// 戻る・進む
navigate(-1); // 1つ前のページに戻る
navigate(1); // 1つ次のページに進む
replace: true
オプションは、ログイン後などで元のページに戻らせたくない場合に使います。
条件付き遷移とリダイレクト
ログインが必要なページにアクセス制限をかけてみましょう。
// components/ProtectedRoute.js
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
function ProtectedRoute({ children }) {
const navigate = useNavigate();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
checkAuthentication();
}, []);
const checkAuthentication = async () => {
try {
const token = localStorage.getItem('authToken');
if (!token) {
navigate('/login', { replace: true });
return;
}
// 実際のプロジェクトでは、トークンの有効性をサーバーで確認
// 今回はダミーで認証成功とする
if (token === 'dummy-token') {
setIsAuthenticated(true);
} else {
localStorage.removeItem('authToken');
navigate('/login', { replace: true });
}
} catch (error) {
console.error('認証エラー:', error);
navigate('/login', { replace: true });
} finally {
setLoading(false);
}
};
if (loading) {
return <div className="loading">認証を確認中...</div>;
}
if (!isAuthenticated) {
return null; // navigate でリダイレクトされるため何も表示しない
}
return children;
}
このProtectedRoute
コンポーネントを使って、ダッシュボードページを保護します。
// pages/DashboardPage.js
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
function DashboardPage() {
const navigate = useNavigate();
const [user, setUser] = useState(null);
const [stats, setStats] = useState({});
useEffect(() => {
fetchUserData();
fetchStats();
}, []);
const fetchUserData = async () => {
try {
// ダミーユーザーデータ
const userData = {
name: "田中太郎",
email: "test@example.com",
avatar: "/images/avatar.jpg"
};
setUser(userData);
} catch (error) {
console.error('ユーザーデータ取得エラー:', error);
}
};
const fetchStats = async () => {
try {
// ダミー統計データ
const statsData = {
totalPosts: 15,
totalLikes: 128,
followersCount: 45
};
setStats(statsData);
} catch (error) {
console.error('統計データ取得エラー:', error);
}
};
const handleLogout = () => {
localStorage.removeItem('authToken');
navigate('/login', { replace: true });
};
const handleProfileEdit = () => {
navigate('/profile/edit');
};
const handleCreatePost = () => {
navigate('/posts/create');
};
if (!user) {
return <div className="loading">ユーザー情報を読み込み中...</div>;
}
return (
<div className="dashboard-page">
<header className="dashboard-header">
<h1>ダッシュボード</h1>
<div className="user-menu">
<span>こんにちは、{user.name}さん</span>
<button onClick={handleProfileEdit}>プロフィール編集</button>
<button onClick={handleLogout}>ログアウト</button>
</div>
</header>
<div className="dashboard-content">
<div className="stats-section">
<h2>統計情報</h2>
<div className="stats-grid">
<div className="stat-card">
<h3>総投稿数</h3>
<p className="stat-number">{stats.totalPosts || 0}</p>
</div>
<div className="stat-card">
<h3>いいね数</h3>
<p className="stat-number">{stats.totalLikes || 0}</p>
</div>
<div className="stat-card">
<h3>フォロワー数</h3>
<p className="stat-number">{stats.followersCount || 0}</p>
</div>
</div>
</div>
<div className="quick-actions">
<h2>クイックアクション</h2>
<div className="actions-grid">
<button onClick={handleCreatePost} className="action-button">
新しい投稿を作成
</button>
<button onClick={() => navigate('/posts')} className="action-button">
投稿一覧を見る
</button>
<button onClick={() => navigate('/followers')} className="action-button">
フォロワーを管理
</button>
<button onClick={() => navigate('/settings')} className="action-button">
設定
</button>
</div>
</div>
</div>
</div>
);
}
export default DashboardPage;
App.jsでProtectedRouteを使ってダッシュボードページを保護します。
// App.js にProtectedRouteを組み込み
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
履歴の管理
ページの閲覧履歴を管理する機能も作ってみましょう。
// components/NavigationWithHistory.js
import { useNavigate, useLocation } from 'react-router-dom';
import { useState, useEffect } from 'react';
function NavigationWithHistory() {
const navigate = useNavigate();
const location = useLocation();
const [history, setHistory] = useState([]);
const [currentIndex, setCurrentIndex] = useState(-1);
useEffect(() => {
// 現在のパスを履歴に追加
setHistory(prev => {
const newHistory = [...prev, location.pathname];
setCurrentIndex(newHistory.length - 1);
return newHistory.slice(-10); // 最新10件のみ保持
});
}, [location.pathname]);
const goBack = () => {
if (currentIndex > 0) {
const previousPath = history[currentIndex - 1];
navigate(previousPath);
} else {
navigate(-1); // ブラウザの履歴で戻る
}
};
const goForward = () => {
if (currentIndex < history.length - 1) {
const nextPath = history[currentIndex + 1];
navigate(nextPath);
} else {
navigate(1); // ブラウザの履歴で進む
}
};
const goHome = () => {
navigate('/');
};
return (
<div className="navigation-with-history">
<div className="navigation-buttons">
<button
onClick={goBack}
disabled={currentIndex <= 0}
className="nav-button"
>
← 戻る
</button>
<button
onClick={goForward}
disabled={currentIndex >= history.length - 1}
className="nav-button"
>
進む →
</button>
<button onClick={goHome} className="nav-button">
🏠 ホーム
</button>
</div>
<div className="breadcrumb">
現在の場所: {location.pathname}
</div>
<div className="history-list">
<h4>閲覧履歴</h4>
<ul>
{history.map((path, index) => (
<li
key={index}
className={index === currentIndex ? 'current' : ''}
onClick={() => navigate(path)}
>
{path}
</li>
))}
</ul>
</div>
</div>
);
}
export default NavigationWithHistory;
このようにして、より高度な画面遷移機能を実装できます。
応用的なルーティング技術
React Routerには、さらに高度な機能もあります。 実際のプロジェクトで役立つ技術を紹介しますね。
ネストしたルート
大きなアプリケーションでは、ページの中にさらに細かいページ分けをしたい場合があります。
// App.js - ネストしたルートの設定
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<HomePage />} />
{/* ユーザー関連のネストしたルート */}
<Route path="/users" element={<UsersLayout />}>
<Route index element={<UsersList />} />
<Route path=":userId" element={<UserProfile />}>
<Route index element={<UserOverview />} />
<Route path="posts" element={<UserPosts />} />
<Route path="followers" element={<UserFollowers />} />
<Route path="following" element={<UserFollowing />} />
</Route>
</Route>
{/* 管理画面のネストしたルート */}
<Route path="/admin" element={<AdminLayout />}>
<Route index element={<AdminDashboard />} />
<Route path="users" element={<AdminUsers />} />
<Route path="posts" element={<AdminPosts />} />
<Route path="settings" element={<AdminSettings />} />
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Router>
);
}
ここでのポイントは、index
とOutlet
の使い方です。
index
: そのルートのデフォルトページOutlet
: 子ルートの内容を表示する場所
// layouts/UsersLayout.js
import { Outlet, Link, useLocation } from 'react-router-dom';
function UsersLayout() {
const location = useLocation();
return (
<div className="users-layout">
<nav className="users-navigation">
<Link
to="/users"
className={location.pathname === '/users' ? 'active' : ''}
>
ユーザー一覧
</Link>
</nav>
<main className="users-content">
<Outlet /> {/* 子ルートのコンテンツが表示される */}
</main>
</div>
);
}
管理画面のレイアウトでは、権限チェックも入れてみましょう。
// layouts/AdminLayout.js
import { Outlet, Link, Navigate } from 'react-router-dom';
import { useState, useEffect } from 'react';
function AdminLayout() {
const [isAdmin, setIsAdmin] = useState(false);
const [loading, setLoading] = useState(true);
useEffect(() => {
checkAdminPermission();
}, []);
const checkAdminPermission = async () => {
try {
const token = localStorage.getItem('authToken');
// 実際のプロジェクトでは、サーバーで管理者権限をチェック
// 今回はダミーで、特定のトークンの場合のみ管理者とする
const isAdminUser = token === 'admin-token';
setIsAdmin(isAdminUser);
} catch (error) {
setIsAdmin(false);
} finally {
setLoading(false);
}
};
if (loading) {
return <div className="loading">権限を確認中...</div>;
}
if (!isAdmin) {
return <Navigate to="/login" replace />;
}
return (
<div className="admin-layout">
<aside className="admin-sidebar">
<h2>管理画面</h2>
<nav className="admin-navigation">
<Link to="/admin">ダッシュボード</Link>
<Link to="/admin/users">ユーザー管理</Link>
<Link to="/admin/posts">投稿管理</Link>
<Link to="/admin/settings">設定</Link>
</nav>
</aside>
<main className="admin-main">
<Outlet />
</main>
</div>
);
}
検索パラメータの活用
URLに検索条件などを含める場合は、検索パラメータ(クエリパラメータ)を使います。
// pages/SearchPage.js
import { useState, useEffect } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate();
const [searchResults, setSearchResults] = useState([]);
const [loading, setLoading] = useState(false);
const [filters, setFilters] = useState({
query: searchParams.get('q') || '',
category: searchParams.get('category') || 'all',
sortBy: searchParams.get('sort') || 'relevance',
page: parseInt(searchParams.get('page')) || 1
});
useEffect(() => {
if (filters.query) {
performSearch();
}
}, [searchParams]);
const performSearch = async () => {
setLoading(true);
try {
// ダミー検索結果
const dummyResults = [
{
id: 1,
title: "React入門ガイド",
snippet: "Reactの基本的な使い方を学ぼう",
category: "記事",
date: "2024-01-15",
url: "/articles/1"
},
{
id: 2,
title: "JavaScriptの基礎",
snippet: "JavaScript初心者向けの解説",
category: "記事",
date: "2024-01-10",
url: "/articles/2"
}
];
// フィルタリング(実際はサーバー側で行う)
let filteredResults = dummyResults;
if (filters.category !== 'all') {
filteredResults = filteredResults.filter(item =>
item.category === filters.category
);
}
setSearchResults(filteredResults);
} catch (error) {
console.error('検索エラー:', error);
} finally {
setLoading(false);
}
};
const updateFilters = (newFilters) => {
const updatedFilters = { ...filters, ...newFilters, page: 1 };
setFilters(updatedFilters);
// URLパラメータを更新
const params = new URLSearchParams();
if (updatedFilters.query) params.set('q', updatedFilters.query);
if (updatedFilters.category !== 'all') params.set('category', updatedFilters.category);
if (updatedFilters.sortBy !== 'relevance') params.set('sort', updatedFilters.sortBy);
if (updatedFilters.page !== 1) params.set('page', updatedFilters.page.toString());
setSearchParams(params);
};
const handleSearchSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const query = formData.get('query');
updateFilters({ query });
};
const handlePageChange = (newPage) => {
updateFilters({ page: newPage });
};
return (
<div className="search-page">
<header className="search-header">
<h1>検索</h1>
<form onSubmit={handleSearchSubmit} className="search-form">
<input
type="text"
name="query"
defaultValue={filters.query}
placeholder="検索キーワードを入力..."
className="search-input"
/>
<button type="submit">検索</button>
</form>
</header>
<div className="search-content">
<aside className="search-filters">
<h3>絞り込み</h3>
<div className="filter-group">
<label>カテゴリ</label>
<select
value={filters.category}
onChange={(e) => updateFilters({ category: e.target.value })}
>
<option value="all">すべて</option>
<option value="記事">記事</option>
<option value="商品">商品</option>
<option value="ユーザー">ユーザー</option>
</select>
</div>
<div className="filter-group">
<label>並び順</label>
<select
value={filters.sortBy}
onChange={(e) => updateFilters({ sortBy: e.target.value })}
>
<option value="relevance">関連度順</option>
<option value="date">日付順</option>
<option value="popularity">人気順</option>
</select>
</div>
</aside>
<main className="search-results">
{loading ? (
<div className="loading">検索中...</div>
) : (
<>
<div className="search-info">
"{filters.query}" の検索結果: {searchResults.length}件
</div>
<div className="results-list">
{searchResults.map(result => (
<div key={result.id} className="result-item">
<h3>
<Link to={result.url}>{result.title}</Link>
</h3>
<p className="result-snippet">{result.snippet}</p>
<div className="result-meta">
<span className="category">{result.category}</span>
<span className="date">{result.date}</span>
</div>
</div>
))}
</div>
</>
)}
</main>
</div>
</div>
);
}
export default SearchPage;
この検索ページでは、URLパラメータを使って検索条件を管理しています。
例えば、/search?q=React&category=記事&sort=date
のようなURLになります。
useSearchParams
フックの使い方はこんな感じです。
const [searchParams, setSearchParams] = useSearchParams();
// パラメータの取得
const query = searchParams.get('q'); // 'React'
const category = searchParams.get('category'); // '記事'
// パラメータの更新
const params = new URLSearchParams();
params.set('q', 'JavaScript');
params.set('category', 'all');
setSearchParams(params);
これで、検索条件をURLで共有したり、ブックマークしたりできます。
よくあるトラブルシューティング
React Routerを使っていると、いくつかのトラブルに遭遇することがあります。 代表的な問題と解決方法を紹介しますね。
リロード時の404エラー
「ページは表示されるけど、リロードすると404エラーになる」という問題がよくあります。
問題の原因
SPAでは実際のHTMLファイルは1つだけなので、サーバーが/products/1
などのパスを認識できません。
解決方法1: 開発環境での対応
// package.json
{
"homepage": "./",
"scripts": {
"build": "react-scripts build",
"serve": "serve -s build -l 3000"
}
}
解決方法2: サーバー設定
Apacheの場合(.htaccessファイル):
Options -MultiViews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.html [QSA,L]
Nginxの場合:
location / {
try_files $uri $uri/ /index.html;
}
解決方法3: HashRouter を使用(開発時のみ推奨)
import { HashRouter as Router } from 'react-router-dom';
function App() {
return (
<Router>
{/* ルート定義 */}
</Router>
);
}
HashRouter
を使うと、URLが/#/products/1
のような形になり、サーバー設定なしでも動作します。
パフォーマンスの最適化
「ページが多くなると初期ロードが遅い」という問題も発生します。
解決方法: 遅延読み込み(Lazy Loading)
// 問題: 全てのページを最初に読み込むため初期ロードが遅い
// 解決方法: React.lazy を使った遅延読み込み
import { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
// ページコンポーネントを遅延読み込み
const HomePage = lazy(() => import('./pages/HomePage'));
const AboutPage = lazy(() => import('./pages/AboutPage'));
const ProductsPage = lazy(() => import('./pages/ProductsPage'));
const ContactPage = lazy(() => import('./pages/ContactPage'));
// ローディングコンポーネント
function LoadingSpinner() {
return (
<div className="loading-spinner">
<div className="spinner"></div>
<p>ページを読み込み中...</p>
</div>
);
}
function App() {
return (
<Router>
<div className="app">
<nav>
{/* ナビゲーション */}
</nav>
<main>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/products" element={<ProductsPage />} />
<Route path="/contact" element={<ContactPage />} />
</Routes>
</Suspense>
</main>
</div>
</Router>
);
}
lazy
とSuspense
を使うことで、必要なページだけを読み込むようになります。
最初にアクセスしたページのみ読み込まれ、他のページは実際にアクセスしたときに読み込まれます。
状態の保持
「ページ遷移すると状態が失われる」という問題もあります。
解決方法: Context API を使用した状態管理
// contexts/AppStateContext.js
import { createContext, useContext, useReducer } from 'react';
const AppStateContext = createContext();
const initialState = {
user: null,
cart: [],
preferences: {}
};
function appStateReducer(state, action) {
switch (action.type) {
case 'SET_USER':
return { ...state, user: action.payload };
case 'ADD_TO_CART':
return { ...state, cart: [...state.cart, action.payload] };
case 'UPDATE_PREFERENCES':
return { ...state, preferences: { ...state.preferences, ...action.payload } };
default:
return state;
}
}
export function AppStateProvider({ children }) {
const [state, dispatch] = useReducer(appStateReducer, initialState);
return (
<AppStateContext.Provider value={{ state, dispatch }}>
{children}
</AppStateContext.Provider>
);
}
export const useAppState = () => {
const context = useContext(AppStateContext);
if (!context) {
throw new Error('useAppState must be used within AppStateProvider');
}
return context;
};
App.jsで全体を囲います。
// App.js
function App() {
return (
<AppStateProvider>
<Router>
{/* ルート定義 */}
</Router>
</AppStateProvider>
);
}
各ページで状態を使用します。
// 使用例
function ProductPage() {
const { state, dispatch } = useAppState();
const addToCart = (product) => {
dispatch({ type: 'ADD_TO_CART', payload: product });
};
return (
<div>
{/* 商品表示 */}
<p>カート内アイテム数: {state.cart.length}</p>
</div>
);
}
これで、ページ遷移しても状態が保持されます。
まとめ:React画面遷移をマスターしよう
React画面遷移について、基本から応用まで詳しく解説しました。
基本概念の理解が重要
SPAの仕組みとReact Routerの役割を理解することで、なぜこの技術が必要なのかが分かります。
React Routerの基本をマスター
BrowserRouter、Routes、Route、Linkの使い方をしっかり覚えましょう。 これができれば、基本的な画面遷移は問題ありません。
動的ルーティングで柔軟性を向上
useParamsフックを使ってURLパラメータを取得できれば、商品詳細ページやユーザープロフィールページなどを作れます。
プログラム制御で高度な操作
useNavigateフックを使えば、ボタンクリックやフォーム送信時の画面遷移も自由自在です。
応用技術で実用性アップ
ネストしたルート、検索パラメータ、遅延読み込みなどを使えば、より実用的なアプリケーションを作れます。
トラブル対応で安心開発
よくある問題と解決方法を知っておけば、困ったときにも対応できます。
React画面遷移は、最初は複雑に感じるかもしれません。 でも、基本をしっかり理解すれば、とても便利で使いやすい仕組みです。
この記事で紹介した方法を参考に、実際にコードを書いて練習してみてください。 きっと、ユーザーにとって使いやすいWebアプリケーションを作ることができるはずです。
ぜひ、手を動かしながら学習を進めてみてくださいね!