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-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 filename="app/dashboard/page.js"
import { Suspense } from 'react'
import SearchBar from './search-bar'
function SearchBarFallback() {
return
}
export default function Page() {
return (
<>
}>
Dashboard
</>
)
}
```jsx filename="app/dashboard/search-bar.js"
'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:
```jsx filename="app/dashboard/page.js"
import SearchBar from './search-bar'
export default async function Page({ searchParams }) {
const { search } = await searchParams
return (
<>
Dashboard
</>
)
}
```jsx filename="app/dashboard/search-bar.js"
'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 filename="app/dashboard/page.js"
import { connection } from 'next/server'
import SearchBar from './search-bar'
export default async function Page() {
await connection()
return (
<>
Dashboard
</>
)
}
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.
## Related
- [Next.js Dynamic Server Usage: the prerender error](https://www.iloveblogs.blog/post/nextjs-dynamic-server-usage-couldnt-be-rendered-statically-fix) — same error family, different cause: accessing dynamic data outside Suspense.
- [Next.js App Router complete guide](https://www.iloveblogs.blog/guides/nextjs-app-router-complete-guide) — covers prerendering, Suspense patterns, and the static/dynamic boundary.
- [Why useEffect runs twice in Next.js dev](https://www.iloveblogs.blog/post/why-useeffect-runs-twice-in-nextjs-dev) — another dev/prod discrepancy in Next.js to keep in mind.
---
*Originally published at [https://www.iloveblogs.blog](https://www.iloveblogs.blog/post/fix-nextjs-missing-suspense-boundary-use-searchparams)*
Top comments (0)