Missing Suspense boundary with useSearchParams in Next.js: 3 fixes
nextjs-supabase

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.

2026-06-27
6 min read
Missing Suspense boundary with useSearchParams in Next.js: 3 fixes

I hit this exact error after upgrading a search UI to App Router. Everything worked perfectly in next dev. The production build died with:

plaintext
Error: Missing Suspense boundary with useSearchParams
Read more: https://nextjs.org/docs/messages/missing-suspense-with-csr-bailout

The 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:

jsx
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>
    </>
  )
}
jsx
'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.

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:

jsx
import SearchBar from './search-bar'
 
export default async function Page({ searchParams }) {
  const { search } = await searchParams
  return (
    <>
      <nav>
        <SearchBar initialSearch={search ?? ''} />
      </nav>
      <h1>Dashboard</h1>
    </>
  )
}
jsx
'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:

jsx
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 build locally and confirm it succeeds (do not rely on next dev).
  • [ ] Verify the Suspense fallback 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 useSearchParams internally.
  • [ ] 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.

Frequently Asked Questions

|

Have more questions? Contact us

Written by

Mahdi Br
Mahdi Br

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — no fluff

RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.