Files
next.js/errors/blocking-prerender-client-hook.mdx
T
Josh StoryandGitHub d424aa8ec4 Stabilize unstable_instant (#94578)
This API has experimental modes for build validation but the dev
validation is going to be stable in the next release.
2026-06-09 13:50:25 -07:00

293 lines
17 KiB
Plaintext

---
title: Next.js encountered URL data in a Client Component outside of Suspense
kind: insight
---
During [prerendering](/docs/app/glossary#prerendering), a Client Component called a navigation hook ([`usePathname`](/docs/app/api-reference/functions/use-pathname), [`useParams`](/docs/app/api-reference/functions/use-params), [`useSearchParams`](/docs/app/api-reference/functions/use-search-params#prerendering), [`useSelectedLayoutSegment`](/docs/app/api-reference/functions/use-selected-layout-segment), or [`useSelectedLayoutSegments`](/docs/app/api-reference/functions/use-selected-layout-segments)) outside of a [`<Suspense>`](https://react.dev/reference/react/Suspense) boundary. With [Cache Components](/docs/app/api-reference/config/next-config-js/cacheComponents) 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](/docs/app/glossary#static-shell).
`useSearchParams` 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 not covered by [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params).
Server-side request-bound reads ([`cookies()`](/docs/app/api-reference/functions/cookies), [`headers()`](/docs/app/api-reference/functions/headers)) have different fixes. See [Next.js encountered runtime data during prerendering](/docs/messages/blocking-prerender-runtime). For unstable values like [`Math.random()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random) in Client Components, see [Next.js encountered the unstable value Math.random() in a Client Component](/docs/messages/blocking-prerender-random-client).
> **Good to know**: In [`next dev`](/docs/app/api-reference/cli/next#next-dev-options), the error overlay points at the failing component. Run `next build --debug-prerender` to get the full list of blocking routes with stack traces. When iterating on specific routes, use `next build --debug-build-paths /dashboard /settings` to rebuild only those pages.
## Ways to fix this
<FixOption
group="stream"
href="#wrap-in-or-move-into-suspense"
prompt={`Wrap the component that calls the navigation hook in <Suspense>. The fallback prop must render synchronous, deterministic JSX (no fetch, no awaiting, no Math.random or Date.now) that approximates the final layout. Import Suspense from "react". Do not change the hook call itself. Place the Suspense boundary as close to the hook call as possible so the rest of the route stays in the prerendered static shell.`}
title="Wrap in or move into Suspense"
>
Wrap the component that calls the hook in a Suspense boundary so Next.js can
prerender a fallback while the runtime value loads.
</FixOption>
<FixOption
group="cache"
href="#for-known-params-prerender"
prompt={`Add a generateStaticParams() export to the page or layout that defines the dynamic segment. Return an array of param objects whose keys match the segment's [param] names. On the generated paths, useParams resolves to a build-time constant, and usePathname and useSelectedLayoutSegment(s) (which derive from the URL path) also resolve without needing a Suspense boundary. Does not help useSearchParams, since search params come from the request URL's query string and are not part of segment params. Do not introduce new imports beyond Next.js types. If you can't return at least one known param at build time, use "Wrap in or move into Suspense" instead.`}
title="For known params, prerender"
>
Tell Next.js the full set of valid params ahead of time. Each one becomes a
prerendered route. Does not help useSearchParams.
</FixOption>
<FixOption
group="block"
href="#allow-blocking-route"
prompt={`Add "export const instant = false" as a top-level export in the page or layout file. This silences the warning for this segment. Confirm with the user that the route is intentionally request-time before applying this change: the export exempts the segment from instant-navigation validation, and the route renders on every request, so navigations to it block until the render completes.`}
title="Allow blocking route"
>
Opt the segment out of instant-navigation validation. The route renders on
every request.
</FixOption>
## Wrap in or move into Suspense
Choose this fix when you want the route to prerender a fallback and replace it with the hook's value at runtime.
### Patterns
#### Wrap the component that calls the hook
Move the hook call into a small Client Component and wrap it in [`<Suspense>`](https://react.dev/reference/react/Suspense). Next.js prerenders the fallback and streams the real value in when it's available.
```jsx filename="app/dashboard/page.js"
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>
)
}
```
```jsx filename="app/dashboard/search.js"
'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](/docs/app/api-reference/functions/use-search-params#prerendering)
#### Push the hook read down to the leaf
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.
```jsx filename="app/layout.js"
import { Nav } from './nav'
export default function Layout({ children }) {
return (
<div>
<Nav />
{children}
</div>
)
}
```
```jsx filename="app/nav.js"
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>
)
}
```
```jsx filename="app/active-dot.js"
'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`](/docs/app/api-reference/functions/use-pathname), [Creating an active link component with `useSelectedLayoutSegment`](/docs/app/api-reference/functions/use-selected-layout-segment#creating-an-active-link-component)
#### Wrap a sibling that uses the hook value
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.
```jsx filename="app/dashboard/page.js"
import { Suspense } from 'react'
import { Header } from './header'
import { TrackPageView } from './track-page-view'
export default function Page() {
return (
<>
<Header />
<Suspense>
<TrackPageView />
</Suspense>
<DashboardContent />
</>
)
}
```
```jsx filename="app/dashboard/track-page-view.js"
'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.
### Trade-off
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 shift surrounding content.
### Gotchas
- Place the [`<Suspense>`](https://react.dev/reference/react/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.
- The fallback must be synchronous and deterministic. Do not call [`Math.random()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random), [`Date.now()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now), [`crypto.randomUUID()`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID), or [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) inside it. Each one raises a separate [Cache Components](/docs/app/api-reference/config/next-config-js/cacheComponents) error during prerendering.
- Do not pass `{children}` through in the fallback. Child pages may include dynamic reads (for example, `/_not-found` calling [`cookies()`](/docs/app/api-reference/functions/cookies) or [`headers()`](/docs/app/api-reference/functions/headers)) that propagate into what should be a static fallback. Render a placeholder that doesn't include `{children}`.
- A nav rendered in a layout suspends on any page below it that reads URL data the layout's static shell doesn't know. Push the hook read down to the smallest leaf that needs the value so the rest of the nav stays prerendered.
- The static shell does not include the active state. The fallback paints first, then the indicator hydrates and the active style appears. For active-link patterns where the flash is visible, add an [inline script that runs before paint](/docs/app/guides/preventing-flash-before-hydration) to set the active attribute from `location.pathname` so the correct link is styled on the first paint.
- [`usePathname`](/docs/app/api-reference/functions/use-pathname#avoid-hydration-mismatch-with-rewrites) 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](/docs/app/api-reference/functions/use-pathname#avoid-hydration-mismatch-with-rewrites).
## For known params, prerender
Choose this fix when you know the valid param values at build time. Adding [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params) tells Next.js to prerender each one. On those prerendered routes, [`useParams`](/docs/app/api-reference/functions/use-params) resolves to a build-time constant, and the [`Suspense` boundary around `usePathname` is optional](/docs/app/api-reference/functions/use-pathname). This fix does not apply to [`useSearchParams`](/docs/app/api-reference/functions/use-search-params), since search params come from the request URL's query string and are not part of the segment params.
### Patterns
#### Export generateStaticParams
Export [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params) from the page or layout that defines the dynamic segment. Return an array of param objects whose keys match the segment's `[param]` names.
```jsx filename="app/blog/[slug]/page.js"
export function generateStaticParams() {
return [{ slug: 'hello-world' }, { slug: 'release-notes' }]
}
```
Learn more: [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params).
### Trade-off
The list of params is decided at build time. See [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params) for rebuild and cache-invalidation patterns when the list changes.
### Gotchas
- [`useSearchParams`](/docs/app/api-reference/functions/use-search-params) is never affected by [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params). Search params come from the request URL's query string. If a route reads `useSearchParams`, you still need a [`<Suspense>`](https://react.dev/reference/react/Suspense) boundary around that read or [Allow blocking route](#allow-blocking-route) on the segment.
- A request for a param value not in the returned list falls through to runtime rendering. The same client hook will suspend on that request unless the segment is wrapped in `<Suspense>` or marked with [Allow blocking route](#allow-blocking-route).
- The generated paths must cover every dynamic segment in the route. If any segment is missing, the route falls back to runtime rendering for that param.
- [`generateStaticParams`](/docs/app/api-reference/functions/generate-static-params) runs at build time, so it cannot depend on per-request values.
- The [`dynamicParams`](/docs/app/api-reference/file-conventions/route-segment-config/dynamicParams) config isn't available in this model. If you're migrating an existing page, use [`<Suspense>`](https://react.dev/reference/react/Suspense), [`notFound()`](/docs/app/api-reference/functions/not-found), or [Allow blocking route](#allow-blocking-route) instead.
## Allow blocking route
Choose this fix when the route renders per-request and there's no useful static shell. Setting [`instant`](/docs/app/api-reference/file-conventions/route-segment-config/instant) to `false` exempts the segment from instant-navigation validation. The page renders on every request and the navigation blocks until that render completes.
### Patterns
#### Opt the page out
Add the export to the page that triggered the error. Only that route blocks.
```jsx filename="app/dashboard/page.js"
export const instant = false
export default function Page() {
return <Dashboard />
}
```
Learn more: [Ensuring instant navigations](/docs/app/guides/instant-navigation).
#### Opt the layout out
When the shared layout itself can't ship instantly (it reads URL data of its own that has no meaningful fallback), set [`instant`](/docs/app/api-reference/file-conventions/route-segment-config/instant) to `false` on the layout. This allows that layout segment to block while descendant segments remain independently validated.
```jsx filename="app/dashboard/layout.js"
export const instant = false
export default function DashboardLayout({ children }) {
return <DashboardShell>{children}</DashboardShell>
}
```
Learn more: [Route segment `instant` config](/docs/app/api-reference/file-conventions/route-segment-config/instant).
Use either pattern when:
- The route needs request-time data high in the tree to decide what to render, so there is no meaningful [static shell](/docs/app/glossary#static-shell) worth showing first.
- You're migrating a route incrementally and want to defer the lifetime decision without changing how the page renders today.
For a client-hook error this is rarely the right answer. The hook reads a small piece of URL data, and a [`<Suspense>`](https://react.dev/reference/react/Suspense) boundary around that read keeps the rest of the route prerendered. Choose [Wrap in or move into Suspense](#wrap-in-or-move-into-suspense) or [For known params, prerender](#for-known-params-prerender) when either is feasible.
### Trade-off
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.
### Gotchas
- Setting [`instant`](/docs/app/api-reference/file-conventions/route-segment-config/instant) to `false` opts only the segment that exports it out. Descendant segments are still validated by the global default. See [Don't want this validation?](#dont-want-this-validation) for the subtree-wide opt-out.
- This export does not disable [prerendering](/docs/app/glossary#prerendering). The route still prerenders if it can. It only silences the instant-navigation validation error.
## Don't want this validation?
Instant-navigation validation runs by default in [Cache Components](/docs/app/api-reference/config/next-config-js/cacheComponents) apps and is what surfaces this error.
- **One segment**: add [`export const instant = false`](/docs/app/api-reference/file-conventions/route-segment-config/instant) to the page or layout file. This opts out the segment itself. Child segments are still validated during client navigations.
- **Layout and its children**: add `export const instant = { unstable_disableValidation: true }` to a layout. This disables validation for that layout and every segment below it.
- **Entire app**: set [`experimental.instantInsights.validationLevel`](/docs/app/api-reference/file-conventions/route-segment-config/instant#configuring-validation-defaults) to `'manual-warning'` in `next.config`. This limits validation to segments that explicitly export `instant`.
See [Ensuring instant navigations](/docs/app/guides/instant-navigation) for the full model.
## Useful links
- [Ensuring instant navigations](/docs/app/guides/instant-navigation)
- [Preventing flash before hydration](/docs/app/guides/preventing-flash-before-hydration)
- [Next.js encountered runtime data during prerendering](/docs/messages/blocking-prerender-runtime)
- [Next.js encountered the unstable value Math.random() in a Client Component](/docs/messages/blocking-prerender-random-client)