Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Fix: useSearchParams() Should Be Wrapped in a Suspense Boundary (and Why It Only Breaks at Build)

2026年10月17日に「Dev.to」が公開したITニュース「Fix: useSearchParams() Should Be Wrapped in a Suspense Boundary (and Why It Only Breaks at Build)」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

Next.jsで`useSearchParams()`を共有コンポーネントで使うと、ビルド時にエラーになる。これは、ビルド時、URL情報が必要なのに静的に作れないためだ。対策は、`useSearchParams()`を使うコンポーネントを`Suspense`で囲み、動的にする範囲を最小限にすること。これにより、ページ全体の静的化を維持できる。

ITニュース解説

Next.jsを使ってWebアプリケーションを開発していると、開発環境(next devコマンドで実行中)では問題なく動いていたのに、いざ本番環境用にビルド(next buildコマンド)しようとするとエラーが発生し、開発者の頭を悩ませることがある。特に、URLのクエリパラメータ、つまりURLの末尾に?q=検索語のように付いている情報を読み取る機能を持つコンポーネントを配置した場合に、この問題は頻繁に起こる。

例として、ウェブサイトのナビゲーションバーに小さな検索ボックスを設置するケースを考えてみよう。この検索ボックスは、ユーザーがURLに直接クエリパラメータを入力した場合に、その値を初期値として表示したいとする。そこでNext.jsの提供するuseSearchParams()というフックを使って、URLからqという名前のクエリパラメータを読み込むコンポーネントを作成し、これをウェブサイト全体のレイアウト(app/layout.tsx)に組み込んだとしよう。

開発環境では問題なく動作し、検索ボックスは期待通りに機能する。しかし、いざ本番環境へのデプロイのためにビルドを開始すると、「useSearchParams() should be wrapped in a suspense boundary」というエラーメッセージとともにビルドが失敗する。さらに奇妙なことに、エラーメッセージには「/about」ページで問題が発生したと書かれている場合がある。しかし、開発者自身はその/aboutページを全く触っていない。なぜこのようなことが起こるのだろうか。

この現象の鍵は、Next.jsがウェブページをどのようにレンダリング(表示)するかという仕組みの違いにある。Next.jsは、ウェブサイトを高速に表示するために、いくつかのレンダリング方法を使い分けている。

一つは「静的レンダリング」と呼ばれる方法だ。これは、ウェブサイトをビルドする際に、あらかじめサーバー側でHTMLファイルを生成しておく方法である。ユーザーがそのページにアクセスすると、すでに用意されているHTMLファイルがそのまま配信されるため、非常に高速にページが表示される。多くのウェブサイトのトップページや会社概要ページなど、ユーザーごとに内容が変わらないページに適している。このような静的HTMLは、ユーザーからのリクエストがないビルド時に一度だけ作られるため、その時点ではURLに特定のクエリパラメータが付いていることはない。

もう一つは「クライアントサイドレンダリング」だ。これは、最小限のHTMLだけを先に配信し、残りのコンテンツはJavaScriptを使ってユーザーのブラウザ側で生成・表示する方法である。ユーザーの操作に応じて動的に内容が変化する部分や、ユーザー固有の情報を表示する部分によく使われる。

開発環境でnext devを実行している間は、Next.jsは基本的に「リクエストごとにページをレンダリングする」モードで動作する。つまり、ユーザーがブラウザで特定のURLにアクセスするたびに、その時点でのURL情報(クエリパラメータなど)を含めてページを生成する。このため、useSearchParams()が要求するURLのクエリパラメータ情報は常に利用可能であり、問題なく動作するのだ。

しかし、本番環境向けのビルド時になると話が変わる。Next.jsは、可能な限り多くのページを「静的レンダリング」しようとする。これはウェブサイトのパフォーマンスを最大化するためのデフォルトの戦略だ。だが、先ほどの検索ボックスのようにuseSearchParams()を使用するコンポーネントは、ビルド時には存在しないURLのクエリパラメータを読み取ろうとするため、静的にレンダリングすることができない。

Next.jsは、このような場合に「静的レンダリングを諦める」(これを「bail out」と呼ぶ)という挙動をする。つまり、そのコンポーネントを含む部分のレンダリングをサーバーサイドで行うのを中断し、代わりにブラウザ側(クライアントサイド)でJavaScriptを使ってレンダリングするように切り替えようとする。

問題は、この「bail out」がどこまで広がるかということだ。もしuseSearchParams()を使うコンポーネントが、特別な「境界線」で囲まれていない場合、Next.jsはどこまでをクライアントサイドでレンダリングすべきか判断できない。もしこの小さなコンポーネントのためにページ全体がクライアントサイドレンダリングに切り替わってしまうと、せっかく事前に生成しようとしていた静的HTMLが無駄になってしまう。Next.jsはこのような「暗黙のパフォーマンス低下」を防ぐために、明示的に開発者に解決を促すエラーとしてビルドを停止させるのだ。この「境界線」の役割を果たすのがSuspenseコンポーネントである。

そして、「触っていないはずの/aboutページでエラーが出る」という現象は、問題のコンポーネントがウェブサイト全体のレイアウト(app/layout.tsx)に配置されていることが原因だ。共有されるレイアウトに問題のあるコンポーネントがあると、そのレイアウトを使用するすべてのページ(/, /about, /pricingなど)のビルドが同時に失敗する。つまり、問題は特定のページにあるのではなく、共有されているコンポーネントにあるということになる。

この問題を解決するには、いくつかの方法がある。

一つ目の、そして最も推奨される解決策は、useSearchParams()を使用するコンポーネントをSuspenseコンポーネントで囲むことだ。 具体的には、app/layout.tsx内でSearchBoxコンポーネントを<Suspense>タグで囲む。

1import { Suspense } from 'react';
2import { SearchBox } from '@/components/SearchBox';
3
4export default function RootLayout({ children }: { children: React.ReactNode }) {
5  return (
6    <html lang="en">
7      <body>
8        <nav>
9          <Suspense fallback={<input disabled placeholder="Search..." />}>
10            <SearchBox />
11          </Suspense>
12        </nav>
13        {children}
14      </body>
15    </html>
16  );
17}

このようにすることで、SearchBoxコンポーネントだけがクライアントサイドでレンダリングされ、それ以外のページコンテンツは引き続き静的なHTMLとして生成される。fallbackプロパティには、SearchBoxがクライアントサイドで準備されるまでの間に表示される内容を指定する。例えば、入力不可の検索ボックスなどを表示しておくと良い。このfallbackは最終的に表示されるコンポーネントと同じサイズにすることで、ページが表示されたときに要素の位置がずれる「レイアウトシフト」を防ぐことができ、ユーザー体験やSEO(検索エンジン最適化)にも良い影響を与える。Suspenseで囲まれた内容は初期HTMLに含まれないため、SEO上重要な情報であれば、この境界線の外に配置することを忘れてはならない。

二つ目の解決策は、もしクエリパラメータが特定のページだけで必要な場合、useSearchParams()フックを直接使わず、そのページのプロップス(引数)としてsearchParamsを受け取る方法だ。

1// app/search/page.tsx
2export default async function SearchPage({
3  searchParams,
4}: {
5  searchParams: Promise<{ q?: string }>;
6}) {
7  const { q } = await searchParams;
8  return <SearchResults query={q ?? ''} />;
9}

この方法を採用すると、そのページはリクエストごとにレンダリングされる「動的なページ」として扱われる。検索結果ページのように、ユーザーのリクエスト内容によって表示が大きく変わるページには非常に適した方法である。Next.js 15からはsearchParamsがPromiseとして渡されるため、awaitで待つ必要がある点に注意が必要だ。

三つ目の解決策は、ルート全体を強制的に動的なページとして扱う方法だ。 これは、ページのファイル内でexport const dynamic = 'force-dynamic';という記述を追加することで実現できる。 この設定を行うと、そのページは常にリクエストごとにサーバーでレンダリングされるようになるため、useSearchParams()に関するエラーは発生しなくなる。しかし、これはページの静的レンダリングを完全に放棄することになるため、本当にそのページがリクエストごとに動的に生成される必要がある場合にのみ使うべきだ。例えば、マーケティング用の静的な情報ページでこれを使ってしまうと、静的レンダリングによる高速化の恩恵を受けられなくなり、パフォーマンスを無駄に低下させてしまうことになる。

どの解決策を選ぶべきかは、コンポーネントやページの役割によって変わる。

  • ナビゲーションバーの検索ボックスやフィルター、タブのように、URLを読み取る小さなインタラクティブなコンポーネントには、Suspenseでそのコンポーネントを囲むのが最適だ。これにより、必要な部分だけをクライアントサイドで動的にし、ページの大部分は静的に保つことができる。
  • ページ全体がURLのクエリ文字列に依存して内容が大きく変わる場合(例:検索結果ページ)は、ページのプロップスとしてsearchParamsを読み込むのが適切である。
  • ページが本質的にリクエストごとに生成される必要がある場合(例:ユーザー認証が必要なダッシュボードなど)は、force-dynamicを設定することを検討する。

ウェブアプリケーション開発において、useSearchParams()のような「リクエスト時のデータ」を必要とするクライアントコンポーネントを扱う際には、そのコンポーネントを必ずSuspenseで囲み、Next.jsにどこまでを動的な部分として扱うべきか明示的に伝えることが重要だ。これにより、ウェブサイトの大部分を高速な静的HTMLとして提供しつつ、必要な部分だけを動的にするという、パフォーマンスと機能性を両立させた開発が可能になる。特にlayout.tsxなど、サイト全体で共有されるコンポーネントにuseSearchParams()を使用している場合は、注意深く確認し、適切なSuspenseの境界線を設定することが求められる。

関連コンテンツ

関連IT用語