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ページでビルドエラーが起きている模様。
エラー文に示されたリンク(↓)によると「useSearchParamsはSuspenseでラップしないとダメよ」とのことだったので、
言われるがままに修正したのですが(↓)
export default function Page() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchParamsComponent />
</Suspense>
);
}
"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は作成していません。。
うーんと思っていると同じエラーに苦しんでいる人を発見しました。
スレッド内の「layout.tsx丸ごとSuspenseでラップするとエラー消えるよ👍」というコメントが高評価だったのでやってみたのですが、確かにエラーは消えたものの流石に乱暴な気がしたので却下しました。(すぐ下でnot optimalって言われてるし)
ただ実装してもいない/404でエラーが起きている以上、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を使用していました。(すっかり忘れていました)
"use client";
import { useResetSearch } from "../hooks/useResetSearch";
export default function Title() {
const { handleReset } = useResetSearch();
return (
<div onClick={handleReset}>
Title
</div>
);
}
export function useResetSearch() {
const currentParams = useSearchParams();
なので、このhooksを使っている<Title>をSuspenseでラップすればOK!
import { Suspense } from "react";
import Title from "../title";
import TitleSkeleton from "../skeletons/titleSkeleton";
const Header = () => {
return (
<>
<Suspense fallback={<TitleSkeleton />}>
<Title />
</Suspense>
</>
);
};
無事にビルドできました。
Suspenseが必要な理由
ドキュメントの通りですが、自分なりの理解を記載しておきます。
まず、Nextはビルド時にuseSearchParamsを見つけるとコンポーネントを親方向へ遡ってSuspenseを探します。そしてuseSearchParamsからSuspenseまでの範囲がClient-side renderingであると判断し、レンダリングを保留するという仕組みのようです。だからSuspenseが存在しないとどこまでがCSRなのか判断できなくなり、エラーになるということですね。
参考
Discussion