Next.js getStaticProps エラーの原因と解決方法【初心者向け完全ガイド】

React / Next.js

Next.js getStaticProps エラーの原因と解決方法【初心者向け完全ガイド】

はじめに

Next.jsは静的サイト生成(SSG)の機能を提供する人気のReactフレームワークです。その中でもgetStaticPropsは非常に重要な機能ですが、使い方を誤るとさまざまなエラーが発生します。

本記事では、getStaticPropsで発生する一般的なエラーの原因から解決方法まで、初心者でも理解できるように詳しく解説します。

getStaticPropsとは

getStaticPropsはNext.jsの特別な関数で、ビルド時にページの静的なHTMLを事前に生成するために使用されます。サーバーサイドレンダリング(SSR)とは異なり、ビルド時に一度だけ実行され、その結果がキャッシュされます。

このアプローチにより、高速なページロード、SEOの改善、サーバー負荷の軽減などのメリットが得られます。

getStaticPropsの主なエラーの原因

1. ビルド時にデータを取得できない

最も一般的なエラーの原因は、ビルド時にデータベースやAPI から正常にデータを取得できないことです。getStaticPropsはビルド時に実行されるため、その時点で外部リソースにアクセスできない場合、エラーが発生します。

2. 環境変数の不正な設定

API キーやデータベースの接続情報などの環境変数が正しく設定されていない場合、getStaticProps内でのリクエストが失敗します。

3. getStaticPropsの戻り値の型が不正

関数は必ず特定の形式のオブジェクトを返す必要があります。propsrevalidateredirectなどのプロパティは、決まった型である必要があります。

4. Dynamic Routes での getStaticPaths の欠落

動的ルーティングを使用している場合、getStaticPropsと併せてgetStaticPathsを実装する必要があります。これを忘れるとビルドエラーが発生します。

5. ページコンポーネントの外でのエクスポート忘れ

getStaticPropsは必ずページコンポーネントと同じファイルにエクスポートする必要があります。別のファイルに記述すると認識されません。

エラーの解決手順

ステップ1: エラーメッセージを確認する

まず、ビルド時のエラーメッセージを詳しく確認しましょう。Next.jsは比較的親切なエラーメッセージを出力します。エラーメッセージには、どのページで、何が原因で失敗したのかが記載されています。

ステップ2: 環境変数を確認する

.env.localファイルが正しく設定されているか確認してください。開発環境では動作していても、本番環境やCI/CDパイプラインで環境変数が設定されていない可能性があります。

ステップ3: API/データベースの接続テスト

Node.jsスクリプトを別途作成して、getStaticProps内で使用しているAPI呼び出しやデータベースクエリが正常に動作するか確認します。

ステップ4: 関数の戻り値を確認する

getStaticPropsの戻り値が正しい形式であることを確認します。TypeScriptを使用している場合は、型定義を活用するとエラーを早期に発見できます。

ステップ5: getStaticPathsの実装確認

動的ルーティングを使用している場合、getStaticPathsが正しく実装されているか確認します。

実装例とコード解説

基本的な実装例

// pages/posts/[id].js

export default function Post({ post }) {
  if (!post) {
    return <div>記事が見つかりません</div>;
  }

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

export async function getStaticProps({ params }) {
  try {
    // APIからデータを取得
    const response = await fetch(
      `https://api.example.com/posts/${params.id}`
    );

    // エラーチェック
    if (!response.ok) {
      return {
        notFound: true, // 404ページを表示
        revalidate: 60, // 60秒後に再生成
      };
    }

    const post = await response.json();

    return {
      props: {
        post,
      },
      revalidate: 3600, // 1時間ごとに再生成
    };
  } catch (error) {
    console.error('Error fetching post:', error);
    return {
      notFound: true,
      revalidate: 60,
    };
  }
}

export async function getStaticPaths() {
  try {
    // 全ての記事IDを取得
    const response = await fetch('https://api.example.com/posts');
    const posts = await response.json();

    // パスを生成
    const paths = posts.map((post) => ({
      params: { id: post.id.toString() },
    }));

    return {
      paths,
      fallback: 'blocking', // 新しいパスはビルド時にSSGで生成
    };
  } catch (error) {
    console.error('Error fetching paths:', error);
    return {
      paths: [],
      fallback: 'blocking',
    };
  }
}

TypeScriptでの型安全な実装

// pages/posts/[id].tsx

import type { GetStaticProps, GetStaticPaths } from 'next';
import type { ParsedUrlQuery } from 'querystring';

interface Post {
  id: string;
  title: string;
  content: string;
}

interface Props {
  post: Post;
}

interface Params extends ParsedUrlQuery {
  id: string;
}

export default function Post({ post }: Props) {
  if (!post) {
    return <div>記事が見つかりません</div>;
  }

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

export const getStaticProps: GetStaticProps<Props, Params> = async ({
  params,
}) => {
  if (!params?.id) {
    return {
      notFound: true,
    };
  }

  try {
    const response = await fetch(
      `https://api.example.com/posts/${params.id}`
    );

    if (!response.ok) {
      return {
        notFound: true,
        revalidate: 60,
      };
    }

    const post: Post = await response.json();

    return {
      props: {
        post,
      },
      revalidate: 3600,
    };
  } catch (error) {
    console.error('Error fetching post:', error);
    return {
      notFound: true,
      revalidate: 60,
    };
  }
};

export const getStaticPaths: GetStaticPaths<Params> = async () => {
  try {
    const response = await fetch('https://api.example.com/posts');
    const posts: Post[] = await response.json();

    const paths = posts.map((post) => ({
      params: { id: post.id },
    }));

    return {
      paths,
      fallback: 'blocking',
    };
  } catch (error) {
    console.error('Error fetching paths:', error);
    return {
      paths: [],
      fallback: 'blocking',
    };
  }
};

環境変数を使用した実装

// pages/api-data.js

export default function Page({ data }) {
  return (
    <div>
      <h1>データ取得例</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

export async function getStaticProps() {
  const apiKey = process.env.API_KEY;
  const apiUrl = process.env.API_URL;

  // 環境変数が設定されているか確認
  if (!apiKey || !apiUrl) {
    console.error('API_KEY or API_URL is not set');
    return {
      notFound: true,
      revalidate: 60,
    };
  }

  try {
    const response = await fetch(`${apiUrl}/data`, {
      headers: {
        'Authorization': `Bearer ${apiKey}`,
      },
    });

    if (!response.ok) {
      throw new Error(`API returned ${response.status}`);
    }

    const data = await response.json();

    return {
      props: {
        data,
      },
      revalidate: 3600,
    };
  } catch (error) {
    console.error('Error in getStaticProps:', error);
    return {
      notFound: true,
      revalidate: 60,
    };
  }
}

フォールバック戦略の実装

// pages/blog/[slug].js

export default function BlogPost({ post, fallback }) {
  // フォールバック中はローディング表示
  if (fallback) {
    return <div>読み込み中...</div>;
  }

  if (!post) {
    return <div>記事が見つかりません</div>;
  }

  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.date}</time>
      <div>{post.content}</div>
    </article>
  );
}

export async function getStaticProps({ params }) {
  try {
    const post = await fetchPost(params.slug);

    if (!post) {
      return {
        notFound: true,
      };
    }

    return {
      props: {
        post,
      },
      revalidate: 86400, // 24時間
    };
  } catch (error) {
    console.error('Error:', error);
    return {
      notFound: true,
      revalidate: 60,
    };
  }
}

export async function getStaticPaths() {
  try {
    // 人気のある記事のみ事前生成
    const popularPosts = await fetchPopularPosts();
    const paths = popularPosts.map((post) => ({
      params: { slug: post.slug },
    }));

    return {
      paths,
      fallback: 'blocking', // その他のパスは必要に応じて生成
    };
  } catch (error) {
    console.error('Error:', error);
    return {
      paths: [],
      fallback: 'blocking',
    };
  }

  async function fetchPost(slug) {
    // 実装例
    const response = await fetch(`https://api.example.com/posts/${slug}`);
    if (!response.ok) return null;
    return response.json();
  }

  async function fetchPopularPosts() {
    // 実装例
    const response = await fetch('https://api.example.com/popular-posts');
    return response.json();
  }
}

よくある間違いと対処法

間違い1: ブラウザ専用APIを使用

// ❌ 間違い
export async function getStaticProps() {
  // localStorageはサーバーで実行されないため使用不可
  const userId = localStorage.getItem('userId');
  // ...
}

// ✅ 正解
export async function getStaticProps() {
  // 環境変数やクエリパラメータを使用
  const userId = process.env.DEFAULT_USER_ID;
  // ...
}

間違い2: 非同期処理の完了を待たない

// ❌ 間違い
export async function getStaticProps() {
  const data = fetch('https://api.example.com/data'); // awaitがない!
  return {
    props: { data },
  };
}

// ✅ 正解
export async function getStaticProps() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  return {
    props: { data },
  };
}

間違い3: getStaticPropsとgetServerSidePropsの混在

// ❌ 間違い(エラーが発生)
export async function getStaticProps() {
  // ...
}

export async function getServerSideProps() {
  // getStaticPropsと一緒に使用できない
}

// ✅ 正解(どちらか一方を使用)
export async function getStaticProps() {
  // ...
}

間違い4: 動的ルートでgetStaticPathsを忘れる

// ❌ 間違い
// pages/items/[id].js
export async function getStaticProps({ params }) {
  // getStaticPathsがないためビルドエラー
  const item = await fetchItem(params.id);
  return { props: { item } };
}

// ✅ 正解
export async function getStaticProps({ params }) {
  const item = await fetchItem(params.id);
  return { props: { item } };
}

export async function getStaticPaths() {
  const items = await fetchAllItems();
  return {
    paths: items.map((item) => ({ params: { id: item.id } })),
    fallback: 'blocking',
  };
}

間違い5: エラーハンドリングがない

// ❌ 間違い(APIが失敗するとビルドが止まる)
export async function getStaticProps() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();
  return { props: { data } };
}

// ✅ 正解
export async function getStaticProps() {
  try {
    const response = await fetch('https://api.example.com/data');
    
    if (!response.ok) {
      throw new Error(`API error: ${response.status}`);
    }
    
    const data = await response.json();
    return {
      props: { data },
      revalidate: 3600,
    };
  } catch (error) {
    console.error('Error in getStaticProps:', error);
    return {
      notFound: true,
      revalidate: 60,
    };
  }
}

デバッグのコツ

1. ログ出力を活用

getStaticProps内のconsole.logはビルド時に表示されます。問題の特定に活用しましょう。

export async function getStaticProps({ params }) {
  console.log('Building page for ID:', params.id); // ビルド時に表示
  // ...
}

2. next build で検証

開発サーバーではなく、本番ビルドコマンドnext buildを実行してエラーを確認してください。

npm run build
# または
yarn build

3. 環境変数の確認

CI/CDパイプラインで環境変数が正しく設定されているか確認します。

echo $API_URL
echo $API_KEY

実装時のベストプラクティス

1. Incremental Static Regeneration(ISR)を活用

revalidateプロパティを設定することで、定期的にページを再生成できます。これにより、データが古くなることを防げます。

2. fallback戦略を適切に選択

  • fallback: false:事前生成したパスのみ利用可能
  • fallback: true:新しいパスはクライアント側で生成(SEO非推奨)
  • fallback: 'blocking':新しいパスはサーバー側で生成(推奨)

3. エラーハンドリングを徹底

すべての外部リソースアクセスをtry-catchで包み、エラー時の処理を明確にしましょう。

4. TypeScriptの活用

TypeScriptを使用することで、型チェックにより多くのエラーをビルド時に発見できます。

まとめ

getStaticPropsは非常に強力な機能ですが、正しく使用しないと様々なエラーが発生します。本記事で紹介したポイントをまとめます。

  • 環境変数の設定を確認:ビルド環境で必要な環境変数が設定されているか確認しましょう
  • エラーハンドリング:API呼び出しなどの外部リソースアクセスは必ずtry-catchで包みます
  • 動的ルートではgetStaticPathsを実装:動的ルーティングを使用する場合は必須です
  • 戻り値の形式を確認:TypeScriptの活用により型チェックできます
  • next buildで検証:開発環境での動作では不十分。本番ビルドで必ず検証します
  • 適切なfallback戦略:ユースケースに応じて最適なfallback値を選択します
  • ISRの活用revalidateを設定して定期的に再生成し、データの鮮度を保ちます

これらのポイントを押さえることで、getStaticPropsのエラーのほとんどを防ぐことができます。困ったときはビルドログを詳しく確認し、段階的にデバッグを進めることが重要です。

Next.jsの静的生成は、適切に使用すれば非常にパフォーマンスの高いWebアプリケーションを構築できます。ぜひこの記事の内容を参考にして、安定した実装を目指してください。

タイトルとURLをコピーしました