useSearchParams() Should Be Wrapped in a Suspense Boundary: Tested
next build stops with "useSearchParams() should be wrapped in a suspense boundary at page "/search"" when a client component reads the query string on a page Next.js is trying to prerender. At build time there is no request, so there are no search params to read. Next.js needs a Suspense boundary to know which part of the page to leave for the browser.
I reproduced it on Next.js 16.4.4, then tried the four usual fixes and fetched each page with ?q=shoes to see what HTML it actually sends. They are not equal: one of them hides your whole page from crawlers.
The error
// app/Search.tsx
"use client";
import { useSearchParams } from "next/navigation";
export function Search() {
const q = useSearchParams().get("q");
return <p>q={q ?? "none"}</p>;
}
// app/bare/page.tsx
import { Search } from "../Search";
export default function Page() {
return <main><h1>bare</h1><Search /></main>;
}⨯ useSearchParams() should be wrapped in a suspense boundary at page "/bare".
Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout
next build exit code: 1next dev served the same page with a 200 and printed nothing. That's why this error usually shows up for the first time in CI or on Vercel, not on your machine. Run next build locally before you push.
What each fix sends
fix route q=shoes in HTML <h1> in HTML
<Suspense> around the component static no (fallback) yes
dynamic = "force-dynamic" dynamic yes yes
read searchParams in the page dynamic yes yes
<Suspense> around {children} in layout static no noFix 1: wrap the component, not the page
import { Suspense } from "react";
import { Search } from "../Search";
export default function Page() {
return (
<main>
<h1>wrapped</h1>
<Suspense fallback={<p>loading</p>}>
<Search />
</Suspense>
</main>
);
}The build passed and the route stayed static. The HTML had the heading and the fallback; the browser filled in q=shoes after JavaScript loaded. Use this when the query string only drives something interactive, like a filter box or highlighted tab, and the rest of the page should stay static and fast. Keep the boundary as tight as possible: everything inside it is missing from the first HTML.
Fix 2 and 3: render it on the server
If the query changes the page's content, such as search results, render it per request. Either mark the page dynamic, or, better, read searchParams in the server page and pass the value down:
// app/server/page.tsx
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ q?: string }>;
}) {
const { q } = await searchParams;
return <main><h1>server</h1><p>q={q ?? "none"}</p></main>;
}Both built as dynamic routes and both put q=shoes in the HTML. The searchParams version doesn't need useSearchParams or a client component at all. In Next.js 15 and 16 searchParams is a Promise, so await it.
The fix that hides your page
A common answer online is to wrap {children} in Suspense in a layout so the error goes away everywhere. It does make the build pass, and the route stays static. But the prerendered HTML for that page was only the layout's fallback: no h1, no content. Every page under that layout that reads search params ships an empty shell to crawlers and to anyone before JavaScript runs.
// app/layoutwrap/layout.tsx: builds, but the page's HTML is just "layout loading"
import { Suspense } from "react";
export default function Layout({ children }: { children: React.ReactNode }) {
return <Suspense fallback={<p>layout loading</p>}>{children}</Suspense>;
}Put the boundary around the component that reads the search params, not around the page.
Which one to use
- The query string changes what the page says (search results, a filtered list Google should see): read searchParams in the server page.
- The query string only drives client-side UI: wrap that one component in Suspense with a fallback the same size as the content.
- Avoid force-dynamic just to silence the error: it makes the whole route dynamic when one component needed it.
- Avoid a Suspense boundary around {children} in a layout.
Related
- Next.js notFound() page empty? What I tested
- Next.js metadata not working? 8 causes I tested
- Next.js development