🫥

useSearchParams()はSuspenseを忘れるな!

に公開

TL;DR

useSearchParams()Suspenseしよう!
useSearchParams()に依存したhooksを使う時も忘れずに!

経緯

next buildしたら以下のエラーが発生しました。

 ⨯ useSearchParams() should be wrapped in a suspense boundary at page "/test". Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
(中略)
Error occurred prerendering page "/test". Read more: https://nextjs.org/docs/messages/prerender-error
Export encountered an error on /test/page: /test, exiting the build.
 ⨯ Static worker exited with code: 1 and signal: null
 ⨯ useSearchParams() should be wrapped in a suspense boundary at page "/404". Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
(中略)
Error occurred prerendering page "/_not-found". Read more: https://nextjs.org/docs/messages/prerender-error
Export encountered an error on /_not-found/page: /_not-found, exiting the build.
 ⨯ Static worker exited with code: 1 and signal: null

どうやら/testページと/404ページでビルドエラーが起きている模様。
エラー文に示されたリンク(↓)によると「useSearchParamsSuspenseでラップしないとダメよ」とのことだったので、
https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout

言われるがままに修正したのですが(↓)

src/app/test/page.tsx
export default function Page() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <SearchParamsComponent />
    </Suspense>
  );
}
src/app/ui/Test/test.tsx
"use client";

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

function TestComponent() {
  const searchParams = useSearchParams();
  console.log("searchParams:", searchParams);

  return <p>test</p>;
}

エラーは変わらず。🧐
そもそもよく考えたらnot-found.tsxは作成していません。。

うーんと思っていると同じエラーに苦しんでいる人を発見しました。
https://github.com/vercel/next.js/discussions/61654#discussioncomment-8820940
スレッド内の「layout.tsx丸ごとSuspenseでラップするとエラー消えるよ👍」というコメントが高評価だったのでやってみたのですが、確かにエラーは消えたものの流石に乱暴な気がしたので却下しました。(すぐ下でnot optimalって言われてるし)

ただ実装してもいない/404でエラーが起きている以上、layout.tsxに問題があるのは間違いなさそうなので、自分のものを改めて見直すと以下のようになっていました。

src/app/layout.tsx
import type { Metadata } from "next";
import Header from "./ui/Headers/header";

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" className="">
      <body>
        <Header />
        {children}
      </body>
    </html>
  );
}

問題はここの<Header />でした。
<Header>配下の<Title>コンポーネントでは自作のhooksであるuseResetSearch()を使っているのですが、それがuseSearchParamsを使用していました。(すっかり忘れていました)

src/app/ui/title.tsx
"use client";

import { useResetSearch } from "../hooks/useResetSearch";

export default function Title() {
  const { handleReset } = useResetSearch();

  return (
    <div onClick={handleReset}>
      Title
    </div>
  );
}
src/app/hooks/useResetSearch.ts
export function useResetSearch() {
  const currentParams = useSearchParams();

なので、このhooksを使っている<Title>をSuspenseでラップすればOK!

src/app/ui/Headers/Header.tsx
import { Suspense } from "react";
import Title from "../title";
import TitleSkeleton from "../skeletons/titleSkeleton";

const Header = () => {
  return (
    <>
      <Suspense fallback={<TitleSkeleton />}>
        <Title />
      </Suspense>
    </>
  );
};

無事にビルドできました。

Suspenseが必要な理由

ドキュメントの通りですが、自分なりの理解を記載しておきます。
https://nextjs.org/docs/app/api-reference/functions/use-search-params
まず、Nextはビルド時にuseSearchParamsを見つけるとコンポーネントを親方向へ遡ってSuspenseを探します。そしてuseSearchParamsからSuspenseまでの範囲がClient-side renderingであると判断し、レンダリングを保留するという仕組みのようです。だからSuspenseが存在しないとどこまでがCSRなのか判断できなくなり、エラーになるということですね。

参考

https://github.com/vercel/next.js/discussions/61654
https://nextjs.org/docs/app/api-reference/functions/use-search-params
https://react.dev/reference/react/Suspense
https://stackoverflow.com/questions/77914354/how-to-fix-next-js-14-1-prerendering-error

Discussion