errors/blocking-prerender-client-hook.mdx
During prerendering, a Client Component called a navigation hook (usePathname, useParams, useSearchParams, useSelectedLayoutSegment, or useSelectedLayoutSegments) outside of a <Suspense> boundary. With Cache Components enabled, Next.js prerenders as much of a route as possible before a request arrives. These hooks read URL data that is not available during prerendering, so the component needs a fallback to include in the static shell.
The useSearchParams hook triggers this error on any prerendered route because search params come from the request URL. The other four hooks trigger it when the route has dynamic params and is rendered per-request.
Server-side request-bound reads (cookies(), headers()) have different fixes. See Next.js encountered runtime data during prerendering. For unstable values like Math.random() in Client Components, see Next.js encountered the unstable value Math.random() in a Client Component.
Choose this fix when you want the route to prerender a fallback and replace it with the hook's value at runtime.
Move the hook call into a small Client Component and wrap it in <Suspense>. Next.js prerenders the fallback and streams the real value in when it's available.
import { Suspense } from 'react'
import { Search } from './search'
export default function Page() {
return (
<main>
<h1>Dashboard</h1>
<Suspense fallback={<p>Loading search...</p>}>
<Search />
</Suspense>
</main>
)
}
'use client'
import { useSearchParams } from 'next/navigation'
export function Search() {
const searchParams = useSearchParams()
return <p>Search: {searchParams.get('q')}</p>
}
Learn more: useSearchParams prerendering behavior
When the hook is read at the top of the tree but only one piece of UI depends on the value, move the read down. The parent stays prerenderable and only the leaf needs a boundary.
import { Nav } from './nav'
export default function Layout({ children }) {
return (
<div>
<Nav />
{children}
</div>
)
}
import { Suspense } from 'react'
import Link from 'next/link'
import { ActiveDot } from './active-dot'
export function Nav() {
return (
<nav>
<Link href="/dashboard">
Dashboard
<Suspense>
<ActiveDot href="/dashboard" />
</Suspense>
</Link>
<Link href="/settings">
Settings
<Suspense>
<ActiveDot href="/settings" />
</Suspense>
</Link>
</nav>
)
}
'use client'
import { usePathname } from 'next/navigation'
export function ActiveDot({ href }) {
const pathname = usePathname()
const isActive = pathname === href || pathname.startsWith(`${href}/`)
return isActive ? <span aria-hidden="true"> •</span> : null
}
The nav links prerender into the static shell with their final href and label. Only the dot suspends, and its empty fallback doesn't shift the layout.
To style the parent <Link> itself instead (for example, bolding the active label), have the leaf set data-active and read it from the parent with has-data-active:font-bold.
Learn more: usePathname, Creating an active link component with useSelectedLayoutSegment
When the hook value drives a non-visual concern (analytics, attribute on a parent), isolate it in a sibling component and wrap that sibling. The visible UI stays in the static shell.
import { Suspense } from 'react'
import { Header } from './header'
import { TrackPageView } from './track-page-view'
export default function Page() {
return (
<>
<Header />
<Suspense>
<TrackPageView />
</Suspense>
<DashboardContent />
</>
)
}
'use client'
import { useEffect } from 'react'
import { usePathname } from 'next/navigation'
export function TrackPageView() {
const pathname = usePathname()
useEffect(() => {
track('pageview', { pathname })
}, [pathname])
return null
}
The sibling renders nothing visible, so an empty fallback is correct. There is no UI to approximate, and the rest of the page stays in the static shell.
The user sees the fallback on the first paint of the suspended region, then it swaps to the real value once the hook resolves after hydration. Choose a fallback shape that matches the final layout so the swap doesn't cause a layout shift. See minimizing layout shift.
<Suspense> boundary as close to the hook call as possible. Wrapping a large subtree forces the entire subtree into the fallback and loses prerendered content.Math.random(), Date.now(), crypto.randomUUID(), or fetch() inside it. Each one raises a separate Cache Components error during prerendering.{children} through in the fallback. Child pages may include dynamic reads (for example, /_not-found calling cookies() or headers()) that propagate into what should be a static fallback. Render a placeholder that doesn't include {children}.location.pathname so the correct link is styled on the first paint.usePathname reads the source path on the server when the request was rewritten in next.config or middleware, while the browser sees the rewritten path. The active state on a rewritten route resolves to the wrong link on the server and corrects itself on hydration. If your app uses rewrites, defer the read until after mount as the docs recommend.Choose this fix when the route renders per-request and there's no useful static shell. Setting instant to false exempts the segment from instant-navigation validation. The page renders on every request and the navigation blocks until that render completes.
Add the export to the page that triggered the error. Only that route blocks.
export const instant = false
export default function Page() {
return <Dashboard />
}
Learn more: Ensuring instant navigations.
When the shared layout itself can't ship instantly (it reads URL data of its own that has no meaningful fallback), set instant to false on the layout. This allows that layout segment to block while descendant segments remain independently validated.
export const instant = false
export default function DashboardLayout({ children }) {
return <DashboardShell>{children}</DashboardShell>
}
Learn more: Route segment instant config.
Use either pattern when:
For a client-hook error this is rarely the right answer. The hook reads a small piece of URL data, and a <Suspense> boundary around that read keeps the rest of the route prerendered. Choose Wrap in or move into Suspense when feasible.
Navigations to this route are not instant. The user waits for the full server render before any HTML arrives. Use this only when that latency is necessary for the route to function.
instant to false opts only the segment that exports it out. Descendant segments are still validated by the global default.After applying a fix, reload the route and confirm the page immediately paints meaningful UI, with any <Suspense> fallbacks covering only the regions that stream in. A <Suspense> boundary placed around the whole page body can pass validation with an empty shell, which defeats the point of an instant navigation.
In next dev, the error overlay points at the failing component with file paths and line numbers. When working from a build instead, the default next build output is more abbreviated. Run next build --debug-prerender for full user-frame stack traces and next build --debug-build-paths /dashboard /settings to iterate on specific routes.
Instant-navigation validation runs by default in Cache Components apps and is what surfaces this error.
export const instant = false to the page or layout file. This opts out the segment itself. Child segments are still validated during client navigations.experimental.instantInsights.validationLevel to 'manual-warning' in next.config. This limits validation to segments that explicitly export instant.See Ensuring instant navigations for the full model.
generateMetadata()generateMetadata()generateViewport()generateViewport()Math.random() while prerenderingMath.random() in a Client ComponentDate.now() while prerenderingDate.now() in a Client Component