useSearchParams Suspense Error: 3 Real Fixes (Next.js 15)
The error only surfaces during the production build — dev mode hides it completely. Here is why, and the three fixes ranked by situation.
Photo by Ilya Pavlov on Unsplash
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
- Next.js Scroll Resets When searchParams Change
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
Fix Dynamic Server Usage Error in Next.js App Router
Next.js throws this when a route it wants to render at build time calls a dynamic function — cookies(), headers(), searchParams, or a no-store fetch. The fix is either opt the route into dynamic rendering, or remove the request-time dependency if it should be static.
Next.js Turbopack Stuck on Compiling? 5 Fixes (2026)
Turbopack stuck on compiling in Next.js 15/16? Learn the exact causes and 5 proven fixes, plus how to disable Turbopack safely with next build --webpack when production builds crash.
Fix 'Next.js 15 requires Node.js 18.18' Build Error
Next.js 15 drops support for older Node versions. Here is how to upgrade to Node.js 18.18.0 or later and fix build errors.
Browse by Topic
Find stories that matter to you.
