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が必要な理由
ドキュメントの通りですが、自分なりの理解を記載しておきます。useSearchParams
を見つけるとコンポーネントを親方向へ遡ってSuspense
を探します。そしてuseSearchParams
からSuspense
までの範囲がClient-side renderingであると判断し、レンダリングを保留するという仕組みのようです。だからSuspense
が存在しないとどこまでがCSRなのか判断できなくなり、エラーになるということですね。
参考
Discussion