🐈

Next.js の App Router での useSearchParams と Suspense の活用

2025/03/12に公開3

問題: ビルド時の useSearchParams エラー

Next.js の App Router を使用したプロジェクトでビルドを実行すると、以下のようなエラーが発生することがあります:

⨯ useSearchParams() should be wrapped in a suspense boundary at page "/auth/login". Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout

このエラーは、useSearchParams()を使用しているコンポーネントが Suspense でラップされていない場合に発生します。

サーバーコンポーネントとクライアントコンポーネントの違い

基本としてNext.js の App Router では、サーバーコンポーネントクライアントコンポーネントという 2 種類のコンポーネントが存在します。

サーバーコンポーネント (Server Components)

  • デフォルトでは、App Router のすべてのコンポーネントはサーバーコンポーネントです
  • サーバー上でのみレンダリングされ、その結果がクライアントに送信されます
  • useState, useEffectなどのクライアントサイドの React フックは使用できません
  • データベースやファイルシステムに直接アクセスできるなど、サーバーサイドの機能を利用できます
  • バンドルサイズに影響しないため、大きなライブラリをインポートしても問題ありません

クライアントコンポーネント (Client Components)

  • ファイルの先頭に "use client" ディレクティブを追加することでクライアントコンポーネントになります
  • ブラウザでレンダリングされ、ユーザーのインタラクションを処理できます
  • useState, useEffect, useSearchParams などの React フックを使用できます
  • ブラウザ API や DOM を操作できます
  • JavaScript バンドルに含まれるため、サイズに注意する必要があります

この 2 つのタイプのコンポーネントが混在する中で、クライアントコンポーネントがサーバーコンポーネントのレンダリングに依存する場合、同期の問題が発生します。ここで Suspense が重要な役割を果たします。

エラーの原因

Next.js のApp Routerでは、特定のクライアントサイドフック(特にuseSearchParams())を使用すると、「クライアントサイドレンダリングへのベイルアウト(CSR bailout)」が発生します。これは、静的生成(SSG)時にURLのクエリパラメータ情報が利用できないため、サーバーでのレンダリングの一部が省略され、クライアントサイドでレンダリングする必要が生じることを意味します。

特に重要なのは、useSearchParams()は他のクライアントサイドフックとは異なる特殊な扱いを受けるという点です。このフックはNext.jsのレンダリングプロセスに特殊な介入を行うため、静的ビルド時に特別な処理が必要になります。この移行をスムーズに行うために、Next.jsはSuspenseの使用を推奨しています。

Suspenseを使用することで:

  1. 静的生成時のクライアントサイドレンダリングへのベイルアウトを適切に処理
  2. ユーザーに適切なローディング状態を表示
  3. サーバーコンポーネントとクライアントコンポーネントの切り替えをスムーズに

解決策: Suspense の導入

エラーを解決するためには、useSearchParams()を使用するコンポーネントを Suspense でラップする必要があります:

修正前のコード(エラーが発生)

"use client";

import { useState, useEffect } from "react";
import { useSearchParams } from "next/navigation";

export default function LoginPage() {
  const searchParams = useSearchParams();
  const [email, setEmail] = useState("");

  // searchParamsを使用するコード...

  return (
    // コンポーネントのレンダリング
  );
}

修正後のコード(エラーを解決)

"use client";

import { useState, useEffect, Suspense } from "react";
import { useSearchParams } from "next/navigation";

// ローディングコンポーネント
const Loading = () => (
  <div className="loading-spinner">読み込み中...</div>
);

// メインコンポーネント
function LoginContent() {
  const searchParams = useSearchParams();
  const [email, setEmail] = useState("");

  // searchParamsを使用するコード...

  return (
    // コンポーネントのレンダリング
  );
}

// エクスポートするページコンポーネント
export default function LoginPage() {
  return (
    <Suspense fallback={<Loading />}>
      <LoginContent />
    </Suspense>
  );
}

実装のポイント

1. コンポーネントの分割

クライアントサイドのフックを使用するコードを別のコンポーネントに分離することで、コードの構造が明確になります。これにより:

  • 責任の分離が明確になる
  • デバッグが容易になる
  • パフォーマンスの最適化がしやすくなる

2. 適切なローディング状態の提供

Suspense のfallbackプロパティには、データロード中に表示する UI を指定します。ユーザーエクスペリエンスを向上させるために、適切なローディングインジケーターを提供することが重要です:

const Loading = () => (
  <Box className="min-h-screen flex items-center justify-center bg-gray-50">
    <div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
  </Box>
);

3. 複数のフックを使用する場合

複数のクライアントサイドフックを使用する場合も、同じコンポーネント内にまとめて Suspense でラップします:

function PageContent() {
  const searchParams = useSearchParams();
  const pathname = usePathname();
  const router = useRouter();

  // これらのフックを使用するコード...
}

export default function Page() {
  return (
    <Suspense fallback={<Loading />}>
      <PageContent />
    </Suspense>
  );
}

実際のエラー解決事例

実際のプロジェクトで発生した useSearchParams エラーとその解決例を紹介します。

事例: 認証システムのログイン・リダイレクト処理

あるプロジェクトでは、ユーザーがログインした後、元々アクセスしようとしていたページにリダイレクトする機能を実装していました。このためにログインページではuseSearchParams()を使用してredirectUrlパラメータを取得していました。

// 問題があるコード
"use client";

function LoginPage() {
  const searchParams = useSearchParams();
  const redirectUrl = searchParams.get("redirectUrl") || "/dashboard";

  // ログイン処理とリダイレクト...

  return <form>{/* ログインフォーム */}</form>;
}

このコードは開発環境では動作していましたが、本番ビルド時に次のエラーが発生しました:

⨯ useSearchParams() should be wrapped in a suspense boundary at page "/auth/login".

解決策

コンポーネントを分割し、useSearchParams を使用する部分を Suspense でラップしました:

"use client";

import { Suspense } from "react";

// ローディングコンポーネント
const LoginFormSkeleton = () => (
  <div className="animate-pulse">
    <div className="h-10 bg-gray-200 rounded mb-4"></div>
    <div className="h-10 bg-gray-200 rounded mb-4"></div>
    <div className="h-10 bg-primary-200 rounded"></div>
  </div>
);

// useSearchParamsを使用するコンポーネント
function LoginForm() {
  const searchParams = useSearchParams();
  const redirectUrl = searchParams.get("redirectUrl") || "/dashboard";

  // ログイン処理とリダイレクト...

  return (
    <form>
      {/* ログインフォーム */}
      <input type="hidden" name="redirectUrl" value={redirectUrl} />
    </form>
  );
}

// エクスポートするメインコンポーネント
export default function LoginPage() {
  return (
    <div className="login-container">
      <h1>ログイン</h1>
      <Suspense fallback={<LoginFormSkeleton />}>
        <LoginForm />
      </Suspense>
    </div>
  );
}

この修正により、ビルドエラーが解消され、ページは正常にレンダリングされるようになりました。また、フォームのロード中に適切なスケルトン UI を表示することで、ユーザーエクスペリエンスも向上しました。

Suspense のパフォーマンスへの影響

Suspense はアプリケーションのパフォーマンスに様々な影響を与えます。適切に使用するとユーザーエクスペリエンスが向上しますが、誤った使用はパフォーマンスの低下を招く可能性があります。

肯定的な影響

  1. ストリーミング SSR の有効化

    • Suspense を使用することで、Next.js は HTML をストリーミングできるようになります
    • 重いコンポーネントの読み込みを待たずに、先に軽いコンポーネントをユーザーに表示できる
  2. First Contentful Paint (FCP)の改善

    • クリティカルな UI をすぐに表示し、データ依存部分は後から表示できる
    • これにより、ページの初期表示が速くなるため、ユーザーは白い画面を長時間見る必要がなくなる
  3. クライアントハイドレーションの最適化

    • Suspense で囲まれたコンポーネントは、必要なときだけハイドレーションされる
    • これにより、ページの初期インタラクティブ時間(TTI)が改善される

潜在的な注意点

  1. 過剰な使用によるオーバーヘッド

    • 小さなコンポーネントを個別に Suspense でラップすると、管理オーバーヘッドが増加する
    • 必要な箇所にのみ Suspense を使用することが重要
  2. フォールバック UI の頻繁な切り替え

    • フォールバック UI とメインコンテンツが頻繁に切り替わると、ユーザーにとって視覚的な不安定さを感じさせる
    • 適切な場所に Suspense を配置し、ユーザー体験を向上させることが重要
  3. バンドルサイズへの影響

    • Suspense と React の新機能を使用するには、React 18 以上が必要
    • 古い React バージョンからアップグレードする場合、バンドルサイズが増加する可能性がある

ベストプラクティス

  • 論理的なセクションごとに Suspense でラップする
  • ネストされた Suspense を適切に使用して、ページのロード順序を制御する
  • デベロッパーツールでパフォーマンスを測定し、Suspense の影響を評価する

React 19 と Suspense の進化

React 19(2024 年 12 月リリース)では、Suspense の機能がさらに強化されています。特に「Pre-warming for suspended trees」という改善により、Suspense の性能がさらに向上しています。

この改善により、Suspense でラップされたコンポーネントがレンダリングされる前に、React はそのコンポーネントツリーの準備を開始できるようになりました。これにより、ユーザーが Suspense の内容を必要とする頃には、すでにレンダリングの準備が整っている可能性が高まります。

Next.js でもこれらの改善が取り込まれることで、useSearchParamsなどのクライアントサイドフックを使用する際のパフォーマンスがさらに向上することが期待されます。

実装例:認証関連ページの修正

以下に、認証関連の 3 つのページ(ログイン、登録、メール確認)で Suspense を実装した例を示します:

ログインページ(/auth/login/page.tsx)

"use client";

import { useState, useEffect, Suspense } from "react";
import { signIn } from "next-auth/react";
import { useRouter, useSearchParams } from "next/navigation";
// その他のインポート...

// ローディングコンポーネント
const Loading = () => (
  <Box className="min-h-screen flex items-center justify-center bg-gray-50">
    <div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
  </Box>
);

// メインコンポーネント
function LoginContent() {
  const router = useRouter();
  const searchParams = useSearchParams();
  // 状態管理とロジック...

  return (
    // UIレンダリング
  );
}

// エクスポートするページコンポーネント
export default function LoginPage() {
  return (
    <Suspense fallback={<Loading />}>
      <LoginContent />
    </Suspense>
  );
}

メール確認ページ(/auth/verify/page.tsx)

"use client";

import { useState, useEffect, useRef, useCallback, Suspense } from "react";
import { useRouter, useSearchParams } from "next/navigation";
// その他のインポート...

// ローディングコンポーネント
const Loading = () => (
  <Box className="min-h-screen flex items-center justify-center bg-gray-50">
    <div className="w-8 h-8 border-4 border-primary border-t-transparent rounded-full animate-spin"></div>
  </Box>
);

// メインコンポーネント
function VerifyEmailContent() {
  const router = useRouter();
  const searchParams = useSearchParams();
  // 状態管理とロジック...

  return (
    // UIレンダリング
  );
}

// エクスポートするページコンポーネント
export default function VerifyEmailPage() {
  return (
    <Suspense fallback={<Loading />}>
      <VerifyEmailContent />
    </Suspense>
  );
}

注意点とベストプラクティス

1. 細分化された Suspense

アプリケーション全体ではなく、必要なコンポーネントにのみ Suspense を適用することで、部分的な更新が可能になり、パフォーマンスが向上します。

2. サーバーコンポーネントの活用

可能な限り、クライアントサイドのフックを使用する必要がないコンポーネントはサーバーコンポーネントとして実装しましょう。これにより、パフォーマンスが向上し、バンドルサイズが削減されます。

3. エラーハンドリング

Suspense と合わせてErrorBoundaryを使用することで、データフェッチングや処理中のエラーを適切に処理できます。

<ErrorBoundary fallback={<ErrorUI />}>
  <Suspense fallback={<Loading />}>
    <MyComponent />
  </Suspense>
</ErrorBoundary>

まとめ

Next.js の App Router でクライアントサイドのフック(useSearchParams()など)を使用する際は、Suspense でコンポーネントをラップすることが重要です。この対応により:

  1. ビルド時のエラーを解消
  2. クライアントサイドレンダリングへのスムーズな移行
  3. ユーザーに適切なローディング状態を提供
  4. コードの構造化と責任の分離

Suspense はただのエラー解決策ではなく、より良いユーザーエクスペリエンスと開発体験を提供するためのツールです。

参考リソース

Discussion

Honey32Honey32

失礼します。

解決策のコード自体は正しいと思うのですが、解説に誤りと見られる点があります

クライアントレンダリングへの bailout(バイアウトではなく、ベイルアウト)が発生するので Suspense を明示的に追加する必要があるのは、 useSearchParams (静的生成時限定)特有の仕様であって、「クライアントコンポーネント用フック」すべてが同様だとは言えなかったはずです。

補足:

レンダリングの仕組み自体に特別な穴を開けている useSearchParams だけが特別であり、それ以外のフックは「クライアントコンポーネントとして、SSR・SG時にプリレンダリングされる場合」においても正常に動作します。

https://nextjs.org/docs/app/api-reference/functions/use-search-params#behavior

wakiywakiy

ご指摘ありがとうございます!

useSearchParams()特有の問題であることを明確にする形で記事の内容修正させていただきました。