Missing Suspense boundary with useSearchParams in Next.js: 3 fixes
The error only surfaces during the production build — dev mode hides it completely. Here is why, and the three fixes ranked by situation.
I hit this exact error after upgrading a search UI to App Router. Everything worked perfectly in next dev. The production build died with:
Error: Missing Suspense boundary with useSearchParams
Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailoutThe confusing part: this error never appears in development. In dev, routes render on-demand, so useSearchParams never suspends. During next build, static pages are prerendered ahead of time — and that is when Next.js hits the wall.
Why this happens#
useSearchParams is a Client Component hook that reads URLSearchParams from the current URL. The problem is that URL query strings are request-time data — they cannot be known at prerender time. When Next.js tries to statically render a page that contains useSearchParams (directly or in a child component), it needs a Suspense boundary to split the page: the static shell can be prerendered, and the dynamic part with useSearchParams gets hydrated client-side.
Without that boundary, Next.js has nowhere to cut — it fails the build rather than silently serving wrong content.
The rule from the Next.js docs (v16.2.9): during production builds, a static page that calls useSearchParams from a Client Component must be wrapped in a Suspense boundary, otherwise the build fails.
Fix 1 — Wrap in Suspense (fastest)#
This is the minimal fix. Wrap the component using useSearchParams in a <Suspense> boundary with a fallback:
import { Suspense } from 'react'
import SearchBar from './search-bar'
function SearchBarFallback() {
return <div className="h-10 w-full animate-pulse bg-gray-100 rounded" />
}
export default function Page() {
return (
<>
<nav>
<Suspense fallback={<SearchBarFallback />}>
<SearchBar />
</Suspense>
</nav>
<h1>Dashboard</h1>
</>
)
}'use client'
import { useSearchParams } from 'next/navigation'
export default function SearchBar() {
const searchParams = useSearchParams()
const search = searchParams.get('search')
return <input defaultValue={search ?? ''} placeholder="Search..." />
}The fallback renders in the prerendered HTML. Once the page hydrates, React replaces it with the real SearchBar.
When to use this: when the component genuinely needs to live in a Client Component and you want to keep the logic self-contained.
Fix 2 — Use the searchParams page prop (recommended for Server Components)#
If the parent Page component is a Server Component (which is the default), you can read search params directly from the searchParams prop and pass the value down as a plain prop. No hook, no Suspense required:
import SearchBar from './search-bar'
export default async function Page({ searchParams }) {
const { search } = await searchParams
return (
<>
<nav>
<SearchBar initialSearch={search ?? ''} />
</nav>
<h1>Dashboard</h1>
</>
)
}'use client'
export default function SearchBar({ initialSearch }) {
return <input defaultValue={initialSearch} placeholder="Search..." />
}One important caveat: Layouts do NOT receive the searchParams prop. Only Pages do. If you need search params inside a Layout, you must use useSearchParams with a Suspense boundary.
When to use this: when the Page already coordinates the data and you want the cleanest code. This is the approach I now default to.
Fix 3 — Force dynamic rendering with connection()#
If the page genuinely should be dynamic (never prerendered), use connection() from next/server in the parent Server Component. This tells Next.js to wait for an actual incoming request before rendering, which makes useSearchParams safe without Suspense:
import { connection } from 'next/server'
import SearchBar from './search-bar'
export default async function Page() {
await connection()
return (
<>
<nav>
<SearchBar />
</nav>
<h1>Dashboard</h1>
</>
)
}The connection() function is the modern replacement for export const dynamic = 'force-dynamic' — which still works but is now deprecated in favor of this more explicit API.
When to use this: when the entire page is inherently dynamic (authenticated dashboards, personalized feeds). You are trading prerender performance for simplicity.
When the fix is the wrong fix#
Do not put connection() on a marketing page or a blog post to silence this error. Forcing dynamic rendering means the page is server-rendered on every request — you lose the static prerender performance that made you choose App Router in the first place.
If useSearchParams appears in a component buried inside a page that is otherwise fully static, the right answer is Fix 1 (Suspense) — not Fix 3. The Suspense boundary lets the static parts prerender while only the search bar is hydrated.
Do not wrap in Suspense with no fallback (fallback={null}). This causes a layout shift (the search bar flashes in after hydration) and can hurt your CLS Core Web Vital. Always provide a skeleton that matches the size of the real component.
Checklist after applying the fix#
- [ ] Run
next buildlocally and confirm it succeeds (do not rely onnext dev). - [ ] Verify the
Suspensefallback matches the height and width of the real component to avoid CLS. - [ ] If using Fix 2 (searchParams prop), confirm the component using the prop does NOT also call
useSearchParamsinternally. - [ ] If using Fix 3 (connection), confirm the page is meant to be dynamic, not static.
- [ ] Deploy and check the production page: the search bar should render correctly on first load without a flash.
Related#
- Next.js Dynamic Server Usage: the prerender error — same error family, different cause: accessing dynamic data outside Suspense.
- Next.js App Router complete guide — covers prerendering, Suspense patterns, and the static/dynamic boundary.
- Why useEffect runs twice in Next.js dev — another dev/prod discrepancy in Next.js to keep in mind.
- How to set the next/image component to 100% height
Frequently Asked Questions
One email a month — no fluff
RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.
Continue Reading
Next.js proxy.js matcher not working: static assets and auth gaps
Without a matcher, proxy.js runs on every request including _next/static and public assets. And Server Functions are not separate routes — a matcher gap silently drops auth coverage.
Supabase getClaims() vs getSession() in server code: the silent auth bug
Your auth looks correct but users can bypass it. The bug: getSession() in server code reads from local storage without revalidating the token against the Auth server.
How to set the next/image component to 100% height
If your <Image / component ignores height: 100% and appears too small or misaligned, the issue is almost always that the parent container lacks an explicit
Browse by Topic
Find stories that matter to you.
