React画面遷移の基本|初心者でも迷わない実装方法まとめ

React初心者向けの画面遷移完全ガイド。React Routerの基本から動的ルーティング、パラメータ受け渡しまで実例で詳しく解説

Learning Next 運営
79 分で読めます

みなさん、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> &gt; 
        <Link to="/products">商品一覧</Link> &gt; 
        <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>
  );
}

ここでのポイントは、indexOutletの使い方です。

  • 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>
  );
}

lazySuspenseを使うことで、必要なページだけを読み込むようになります。

最初にアクセスしたページのみ読み込まれ、他のページは実際にアクセスしたときに読み込まれます。

状態の保持

「ページ遷移すると状態が失われる」という問題もあります。

解決方法: 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アプリケーションを作ることができるはずです。

ぜひ、手を動かしながら学習を進めてみてくださいね!

関連記事