Documentation Next.js

はじめに

Next.jsで開発を行う際、様々なエラーに遭遇することがあります。この記事では、よく発生するエラーとその解決方法を、具体的なエラーメッセージとコード例を交えて解説します。

Hydration Error

エラーメッセージ

Error: Hydration failed because the initial UI does not match what was rendered on the server.

原因と解決方法

サーバーとクライアントでレンダリング結果が異なる場合に発生します。

// ❌ 悪い例:サーバーとクライアントで異なる値
function Component() {
  return <div>{new Date().toLocaleString()}</div>; // 時刻が異なる!
}

// ✅ 良い例:useEffectでクライアントのみで実行
'use client';

import { useState, useEffect } from 'react';

function Component() {
  const [time, setTime] = useState<string>('');

  useEffect(() => {
    setTime(new Date().toLocaleString());
  }, []);

  return <div>{time || 'Loading...'}</div>;
}
// ❌ 悪い例:typeof windowのチェック
function Component() {
  if (typeof window !== 'undefined') {
    return <div>Client</div>;
  }
  return <div>Server</div>; // Hydrationエラー!
}

// ✅ 良い例:useEffectとstateを使用
'use client';

import { useState, useEffect } from 'react';

function Component() {
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  return <div>{isClient ? 'Client' : 'Loading...'}</div>;
}

Server Component / Client Component関連エラー

エラーメッセージ

Error: useState only works in Client Components. Add the "use client" directive.

解決方法

// ❌ 悪い例:サーバーコンポーネントでuseStateを使用
import { useState } from 'react';

export default function Page() {
  const [count, setCount] = useState(0); // エラー!
  return <div>{count}</div>;
}

// ✅ 良い例:クライアントコンポーネントに分離
// components/Counter.tsx
'use client';

import { useState } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  );
}

// app/page.tsx(サーバーコンポーネント)
import { Counter } from '@/components/Counter';

export default function Page() {
  return (
    <main>
      <h1>My Page</h1>
      <Counter />
    </main>
  );
}

Props経由でのシリアライズエラー

Error: Only plain objects can be passed to Client Components from Server Components
// ❌ 悪い例:Dateオブジェクトを直接渡す
// app/page.tsx (Server Component)
export default async function Page() {
  const data = await fetchData();
  return <ClientComponent date={data.createdAt} />; // Dateオブジェクト
}

// ✅ 良い例:シリアライズ可能な形式に変換
export default async function Page() {
  const data = await fetchData();
  return <ClientComponent dateString={data.createdAt.toISOString()} />;
}

// ClientComponent.tsx
'use client';

export function ClientComponent({ dateString }: { dateString: string }) {
  const date = new Date(dateString);
  return <div>{date.toLocaleDateString()}</div>;
}

Module Not Found エラー

エラーメッセージ

Module not found: Can't resolve 'fs'

解決方法

// ❌ 悪い例:クライアントコンポーネントでNode.jsモジュールを使用
'use client';

import fs from 'fs'; // エラー!

// ✅ 良い例:サーバーサイドでのみ使用
// lib/files.ts (サーバーサイドのみで使用)
import fs from 'fs';
import path from 'path';

export function getFiles() {
  const dir = path.join(process.cwd(), 'data');
  return fs.readdirSync(dir);
}

// app/page.tsx (Server Component)
import { getFiles } from '@/lib/files';

export default async function Page() {
  const files = getFiles();
  return <ul>{files.map((file) => <li key={file}>{file}</li>)}</ul>;
}

パスエイリアスの問題

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@/components/*": ["./src/components/*"]
    }
  }
}

動的ルーティングエラー

エラーメッセージ

Error: getStaticPaths is required for dynamic SSG pages

解決方法

// app/posts/[slug]/page.tsx

// generateStaticParams を定義
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then((res) =>
    res.json()
  );

  return posts.map((post: { slug: string }) => ({
    slug: post.slug,
  }));
}

// 動的メタデータ
export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}) {
  const post = await getPost(params.slug);

  return {
    title: post.title,
    description: post.excerpt,
  };
}

export default async function PostPage({
  params,
}: {
  params: { slug: string };
}) {
  const post = await getPost(params.slug);

  if (!post) {
    notFound();
  }

  return <article>{/* ... */}</article>;
}

環境変数エラー

エラーメッセージ

Error: Environment variable not found: DATABASE_URL

解決方法

// lib/env.ts - 環境変数のバリデーション
const requiredEnvVars = ['DATABASE_URL', 'API_SECRET'] as const;

type EnvVars = (typeof requiredEnvVars)[number];

function getEnv(key: EnvVars): string {
  const value = process.env[key];

  if (!value) {
    throw new Error(`Missing required environment variable: ${key}`);
  }

  return value;
}

export const env = {
  databaseUrl: getEnv('DATABASE_URL'),
  apiSecret: getEnv('API_SECRET'),
  // クライアント用(NEXT_PUBLIC_プレフィックス必須)
  publicApiUrl: process.env.NEXT_PUBLIC_API_URL || '',
};
# .env.local
DATABASE_URL=postgresql://localhost:5432/mydb
API_SECRET=secret-key

# クライアントで使用する場合はNEXT_PUBLIC_プレフィックスが必要
NEXT_PUBLIC_API_URL=https://api.example.com

fetch関連エラー

エラーメッセージ

TypeError: fetch failed

解決方法

// lib/api.ts
export async function fetchWithRetry(
  url: string,
  options?: RequestInit,
  retries = 3
): Promise<Response> {
  for (let i = 0; i < retries; i++) {
    try {
      const response = await fetch(url, {
        ...options,
        // タイムアウト設定
        signal: AbortSignal.timeout(10000),
      });

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }

      return response;
    } catch (error) {
      if (i === retries - 1) throw error;

      // 指数バックオフ
      await new Promise((resolve) =>
        setTimeout(resolve, Math.pow(2, i) * 1000)
      );
    }
  }

  throw new Error('Max retries exceeded');
}

// 使用例
export async function getPosts() {
  try {
    const response = await fetchWithRetry('https://api.example.com/posts');
    return response.json();
  } catch (error) {
    console.error('Failed to fetch posts:', error);
    return [];
  }
}

CORSエラー

エラーメッセージ

Access to fetch at 'http://api.example.com' from origin 'http://localhost:3000' has been blocked by CORS policy

解決方法

// app/api/proxy/route.ts - プロキシAPIを作成
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url);
  const targetUrl = searchParams.get('url');

  if (!targetUrl) {
    return NextResponse.json({ error: 'URL is required' }, { status: 400 });
  }

  try {
    const response = await fetch(targetUrl);
    const data = await response.json();

    return NextResponse.json(data);
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to fetch' },
      { status: 500 }
    );
  }
}
// middleware.ts - CORS設定
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // APIルートにCORSヘッダーを追加
  if (request.nextUrl.pathname.startsWith('/api')) {
    response.headers.set('Access-Control-Allow-Origin', '*');
    response.headers.set(
      'Access-Control-Allow-Methods',
      'GET, POST, PUT, DELETE, OPTIONS'
    );
    response.headers.set(
      'Access-Control-Allow-Headers',
      'Content-Type, Authorization'
    );
  }

  return response;
}

メモリ不足エラー

エラーメッセージ

FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

解決方法

// package.json
{
  "scripts": {
    "build": "NODE_OPTIONS='--max-old-space-size=4096' next build",
    "dev": "NODE_OPTIONS='--max-old-space-size=4096' next dev"
  }
}
// next.config.js - バンドル最適化
module.exports = {
  experimental: {
    optimizePackageImports: ['lodash', 'date-fns', '@mui/material'],
  },
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.optimization.splitChunks = {
        chunks: 'all',
        maxSize: 200000,
      };
    }
    return config;
  },
};

画像最適化エラー

エラーメッセージ

Error: Invalid src prop on `next/image`, hostname "example.com" is not configured

解決方法

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'example.com',
        pathname: '/images/**',
      },
      {
        protocol: 'https',
        hostname: '**.cloudinary.com',
      },
    ],
  },
};

デバッグのベストプラクティス

エラーバウンダリの実装

// app/error.tsx
'use client';

import { useEffect } from 'react';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // エラーをログサービスに送信
    console.error('Application error:', error);
  }, [error]);

  return (
    <div className="error-container">
      <h2>エラーが発生しました</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>再試行</button>
    </div>
  );
}

開発時のデバッグ設定

// next.config.js
module.exports = {
  // ソースマップを有効化
  productionBrowserSourceMaps: true,

  // 詳細なエラーメッセージ
  onDemandEntries: {
    maxInactiveAge: 60 * 1000,
    pagesBufferLength: 5,
  },

  logging: {
    fetches: {
      fullUrl: true,
    },
  },
};

まとめ

Next.jsでよく発生するエラーへの対処方法をまとめます。

エラー種別主な原因対処法
Hydration Errorサーバー/クライアントの不一致useEffectで動的な値を設定
Client Component Erroruse clientディレクティブ不足ファイル先頭に’use client’を追加
Module Not FoundNode.jsモジュールのクライアント使用サーバーサイドでのみ使用
動的ルーティングgenerateStaticParams未定義静的パスを生成
CORSクロスオリジンリクエストプロキシAPI or ヘッダー設定

参考文献

円