Reactでページ更新すると404|SPAの仕組みを理解する

ReactのSPAでページ更新時に404エラーが発生する原因と解決方法を詳しく解説。サーバー設定とルーティングの仕組みを初心者向けに説明します。

Learning Next 運営
26 分で読めます

みなさん、ReactでSPA(Single Page Application)を作成した際、ページを更新すると404エラーが発生して困ったことはありませんか?

「React Routerでページ遷移は正常に動作するのに、F5で更新すると404になる」 「デプロイ後に直接URLにアクセスすると404が表示される」 「開発環境では問題ないのに、本番環境だけエラーになる」

このような問題を経験したことはありませんか?

こうした問題は、SPAの仕組みを理解すれば解決できます。 この記事では、SPAで発生する404エラーの原因と、各種サーバーでの対処法を詳しく解説します。

SPAで404エラーが発生する理由

SPAの基本的な仕組み

SPAは単一のHTMLファイルを基にして、JavaScriptで動的にページの内容を変更します。

簡単に言うと、実際にはページが1つしかないのに、まるで複数のページがあるように見せる技術です。

// React Routerの例
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
      </Routes>
    </Router>
  );
}

このコードでは、/about/contactのURLが表示されますが、実際にはこれらのページファイルは存在しません。 すべてJavaScriptで動的に作られているんです。

404エラーが発生する流れ

どうして404エラーが発生するのか、流れを見てみましょう。

  1. 通常のページ遷移:React Router内でのナビゲーション
  2. ページ更新:ブラウザがサーバーに /about を直接リクエスト
  3. サーバーの応答/about というファイルが存在しないため404を返す

つまり、サーバーは React Router の存在を知らないため、直接URLにアクセスされると404エラーが発生するんです。

通常のWebサイトとSPAの違い

この違いを理解することが重要です。

通常のWebサイト

  • /about にアクセス → about.html ファイルを返す
  • ファイルが存在するので正常に表示される

SPA(React)

  • /about にアクセス → about.html ファイルは存在しない
  • サーバーは404エラーを返す
  • 本来は index.html を返してReact Routerに処理を任せるべき

この仕組みを理解すると、解決策も見えてきますよね。

各種サーバーでの対処法

Apache (.htaccess)

Apacheサーバーを使用している場合の設定方法です。

# .htaccess
Options -MultiViews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.html [QSA,L]

この設定を詳しく見てみましょう。

各行の意味

Options -MultiViews

この行で、Apacheの自動コンテンツネゴシエーションを無効にします。

RewriteEngine On

URLリライト機能を有効にします。

RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.html [QSA,L]

「ファイルが存在しない場合は、すべてのリクエストを index.html にリダイレクト」という設定です。 これにより、存在しないファイルへのリクエストを index.html にリダイレクトします。

Nginx

Nginxサーバーでの設定例です。

# nginx.conf
server {
  listen 80;
  server_name your-domain.com;
  root /var/www/html;
  index index.html;

  location / {
    try_files $uri $uri/ /index.html;
  }
}

Nginxの設定も詳しく見てみましょう。

try_filesディレクティブの説明

try_files $uri $uri/ /index.html;

この設定は以下の順序でファイルを探します:

  1. $uri - リクエストされたファイルをそのまま探す
  2. $uri/ - ディレクトリとして探す
  3. /index.html - 上記で見つからない場合は index.html を返す

つまり、静的ファイルは正常に返し、存在しないルートは index.html に任せるということです。

Node.js (Express)

Node.jsのExpressフレームワークでの設定例です。

// server.js
const express = require('express');
const path = require('path');
const app = express();

// 静的ファイルの配信
app.use(express.static(path.join(__dirname, 'build')));

// すべてのルートでindex.htmlを返す
app.get('/*', function (req, res) {
  res.sendFile(path.join(__dirname, 'build', 'index.html'));
});

const port = process.env.PORT || 5000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

このExpressサーバーの仕組みを説明しましょう。

静的ファイルの配信

app.use(express.static(path.join(__dirname, 'build')));

buildフォルダ内の静的ファイル(CSS、JS、画像など)を配信します。

フォールバックルート

app.get('/*', function (req, res) {
  res.sendFile(path.join(__dirname, 'build', 'index.html'));
});

すべてのGETリクエストに対して、最終的に index.html を返します。 この設定により、React Routerがルーティングを処理できるようになります。

主要ホスティングサービスでの設定

Netlify

Netlifyでの設定方法をご紹介します。

方法1: netlify.toml ファイル

# netlify.toml
[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

方法2: _redirects ファイル

プロジェクトの public フォルダに _redirects ファイルを作成します:

/*    /index.html   200

この設定により、すべてのリクエストが index.html にリダイレクトされます。 ステータスコード200で返すことで、SEOにも配慮した設定になります。

Vercel

Vercelでの設定は vercel.json ファイルで行います。

{
  "rewrites": [
    {
      "source": "/(.*)",
      "destination": "/index.html"
    }
  ]
}

この設定により、すべてのルートが index.html にリライトされます。 Vercelでは rewrites を使うことで、URLを変更せずに内部的にファイルを変更できます。

Firebase Hosting

Firebase Hostingでの設定は firebase.json ファイルで行います。

{
  "hosting": {
    "public": "build",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

設定の詳細

"public": "build"

デプロイするディレクトリを指定します。

"rewrites": [
  {
    "source": "**",
    "destination": "/index.html"
  }
]

すべてのパス(**)を index.html にリライトします。

GitHub Pages

GitHub Pagesは少し特殊な設定が必要です。

package.json の設定

// package.json
{
  "homepage": "https://yourusername.github.io/repository-name",
  "scripts": {
    "predeploy": "npm run build",
    "deploy": "gh-pages -d build"
  }
}

404.html ファイルの作成

GitHub Pagesでは、public フォルダに 404.html ファイルを作成してSPAに対応します:

<!-- public/404.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Redirecting...</title>
    <script type="text/javascript">
      var pathSegmentsToKeep = 1;
      var l = window.location;
      l.replace(
        l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
        l.pathname.split('/').slice(0, 1 + pathSegmentsToKeep).join('/') + 
        '/?/' + l.pathname.slice(1).split('/').slice(pathSegmentsToKeep).join('/').replace(/&/g, '~and~') + 
        (l.search ? '&' + l.search.slice(1).replace(/&/g, '~and~') : '') +
        l.hash
      );
    </script>
  </head>
  <body>
  </body>
</html>

このスクリプトは、404エラーが発生した場合に適切なURLにリダイレクトする仕組みです。 少し複雑ですが、GitHub Pagesの制約に対応するための方法です。

開発環境での設定

Create React App

Create React Appを使用している場合の設定です。

// src/index.js
import { BrowserRouter } from 'react-router-dom';

// 開発サーバーは自動的にSPAに対応
// historyAPIFallback が有効になっている

Create React Appでは、開発サーバーが自動的にSPAに対応してくれます。 そのため、特別な設定は必要ありません。

Vite

Viteを使用している場合の設定方法です。

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    historyApiFallback: true
  }
});

historyApiFallbackの役割

server: {
  historyApiFallback: true
}

この設定により、存在しないルートへのリクエストを index.html にフォールバックします。 これで開発環境でも本番環境と同じような動作になります。

Webpack

Webpackを直接使用している場合の設定です。

// webpack.config.js
module.exports = {
  // ...
  devServer: {
    historyApiFallback: true
  }
};

Webpackでも historyApiFallback を有効にすることで、SPAに対応できます。

React Router の設定

BrowserRouter vs HashRouter

React Routerには2つの主要なルーターがあります。

BrowserRouter(推奨)

// BrowserRouter(推奨)
import { BrowserRouter as Router } from 'react-router-dom';

function App() {
  return (
    <Router>
      {/* ルート設定 */}
    </Router>
  );
}

HashRouter(サーバー設定不要)

// HashRouter(サーバー設定不要)
import { HashRouter as Router } from 'react-router-dom';

function App() {
  return (
    <Router>
      {/* ルート設定 */}
    </Router>
  );
}

それぞれの特徴

BrowserRouter

  • URL: https://example.com/about
  • サーバー設定が必要
  • SEOに適している
  • より自然なURL

HashRouter

  • URL: https://example.com/#/about
  • サーバー設定不要
  • SEOに不利
  • # が含まれるURL

基本的には BrowserRouter の使用をおすすめします。 ただし、サーバー設定ができない場合は HashRouter を使うこともできます。

basename の設定

サブディレクトリにデプロイする場合の設定方法です。

// サブディレクトリにデプロイする場合
<BrowserRouter basename="/my-app">
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
  </Routes>
</BrowserRouter>

basenameの役割

<BrowserRouter basename="/my-app">

この設定により、https://example.com/my-app/about のようなURLに対応できます。 サブディレクトリでホスティングする場合に必要な設定です。

実際の実装例

基本的なルーティング設定

実際のReactアプリでの実装例をご紹介します。

// App.js
import React from 'react';
import {
  BrowserRouter as Router,
  Routes,
  Route,
  Link,
  Navigate
} from 'react-router-dom';

const Home = () => <h1>ホーム</h1>;
const About = () => <h1>会社概要</h1>;
const Contact = () => <h1>お問い合わせ</h1>;
const NotFound = () => <h1>404 - ページが見つかりません</h1>;

function App() {
  return (
    <Router>
      <nav>
        <Link to="/">ホーム</Link>
        <Link to="/about">会社概要</Link>
        <Link to="/contact">お問い合わせ</Link>
      </nav>
      
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
        <Route path="/404" element={<NotFound />} />
        <Route path="*" element={<Navigate to="/404" replace />} />
      </Routes>
    </Router>
  );
}

export default App;

このコードの重要なポイントを見てみましょう。

ナビゲーション部分

<nav>
  <Link to="/">ホーム</Link>
  <Link to="/about">会社概要</Link>
  <Link to="/contact">お問い合わせ</Link>
</nav>

Link コンポーネントを使用することで、ページリロードなしでルーティングができます。

キャッチオールルート

<Route path="*" element={<Navigate to="/404" replace />} />

定義されていないすべてのルートを404ページにリダイレクトします。

動的ルーティング

URLパラメータを使用した動的ルーティングの例です。

// 動的ルーティングの例
import { useParams } from 'react-router-dom';

const UserProfile = () => {
  const { userId } = useParams();
  
  return (
    <div>
      <h1>ユーザープロフィール</h1>
      <p>ユーザーID: {userId}</p>
    </div>
  );
};

// App.js内でのルート設定
<Route path="/user/:userId" element={<UserProfile />} />

useParamsフックの使用

const { userId } = useParams();

URLパラメータを取得して、動的にコンテンツを表示できます。 /user/123 にアクセスすると、userId"123" が入ります。

デバッグ方法

1. ブラウザでの確認

問題の原因を特定するための確認方法です。

// 開発者ツールでの確認
// 1. Network タブでリクエストを確認
// 2. Console でエラーメッセージを確認
// 3. History APIの動作を確認

console.log('現在のURL:', window.location.pathname);
console.log('History state:', window.history.state);

確認すべきポイント

  • ページ更新時にどのようなリクエストが送信されるか
  • サーバーがどのようなレスポンスを返すか
  • JavaScriptエラーが発生していないか

2. サーバーログの確認

サーバー側でのログ確認方法です。

# Apacheのログ確認
tail -f /var/log/apache2/access.log

# Nginxのログ確認
tail -f /var/log/nginx/access.log

ログを確認することで、どのようなリクエストが来ているかがわかります。

3. 設定ファイルの確認

React Router の設定を確認するためのデバッグコンポーネントです。

// React Router の設定確認
const DebugRouter = () => {
  const location = useLocation();
  
  useEffect(() => {
    console.log('Current location:', location.pathname);
  }, [location]);
  
  return null;
};

// App.js内で使用
<Router>
  <DebugRouter />
  <Routes>
    {/* ルート設定 */}
  </Routes>
</Router>

この方法で、ルーティングの動作を詳しく追跡できます。

よくある問題と解決法

問題1: 一部のページで404が発生

よくある間違いとその解決方法です。

// 問題:相対パスでのリンク
<Link to="about">会社概要</Link>

// 解決:絶対パスでのリンク
<Link to="/about">会社概要</Link>

なぜ問題が起こるのか

相対パス "about" を使用すると、現在のURLに追加される形になります。 例えば、/contact ページにいる場合、/contact/about のようなURLになってしまいます。

正しい書き方

絶対パス "/about" を使用することで、常に正しいURLになります。

問題2: アセットファイルが読み込めない

CSS や画像ファイルが読み込めない場合の対処法です。

// package.json での設定
{
  "homepage": "https://yourdomain.com",
  "scripts": {
    "build": "react-scripts build"
  }
}

// 相対パスの設定
{
  "homepage": "."
}

homepageフィールドの役割

"homepage": "https://yourdomain.com"

この設定により、ビルド時にアセットファイルの正しいパスが生成されます。

問題3: サブディレクトリでのデプロイ

サブディレクトリにデプロイする場合の正しい設定方法です。

// 正しい設定
<BrowserRouter basename="/subdirectory">
  <Routes>
    <Route path="/" element={<Home />} />
  </Routes>
</BrowserRouter>

// package.json
{
  "homepage": "https://yourdomain.com/subdirectory"
}

basenameとhomepageの組み合わせ

両方を正しく設定することで、サブディレクトリでも問題なく動作します。

問題4: 環境によって動作が異なる

開発環境と本番環境で動作が異なる場合の対処法です。

// 環境に応じた設定
const basename = process.env.NODE_ENV === 'production' 
  ? '/my-app' 
  : '';

<BrowserRouter basename={basename}>
  <Routes>
    {/* ルート設定 */}
  </Routes>
</BrowserRouter>

環境変数を使用することで、環境に応じて動的に設定を変更できます。

まとめ

ReactのSPAで404エラーが発生する問題は、以下の対処により解決できます:

基本的な解決方法

  1. サーバー設定:すべてのルートを index.html にリダイレクト
  2. ホスティングサービス:各サービスに応じた設定ファイルを作成
  3. React Router:適切なルーティング設定
  4. ビルド設定homepagebasename の正確な設定

重要なポイント

  • SPAの仕組みを理解することで、404エラーの原因が明確になります
  • 開発環境と本番環境で同様の設定が必要です
  • 各ホスティングサービスには独自の設定方法があります

トラブルシューティングのコツ

  • ブラウザの開発者ツールでネットワークタブを確認
  • サーバーログでリクエストの流れを追跡
  • 段階的に設定を確認して問題を特定

404エラーは初心者が必ずと言っていいほど遭遇する問題です。 でも大丈夫です!仕組みを理解すれば、必ず解決できます。

ぜひ今回の対処法を参考に、スムーズに動作するSPAを構築してみてください。

関連記事