Files
next.js/errors/blocking-prerender-client-hook.mdx
T
Aurora ScharffandGitHub 0a518a84e9 instant: build fix-card Copy AI prompt dynamically from title + link (#94637)
### What

The dev-overlay fix cards' `Copy AI prompt` button used to copy a
multi-paragraph instruction string baked into each card (71 prompts
across docs + runtime). This PR replaces that with a dynamic template
the button builds at click time:

```
Apply the [Stream] "Wrap in or move into Suspense" fix to the Next.js Insight raised in this project.

Steps:

1. The failing code is in the error block below — it may be a data-access call, a hook call, a metadata or viewport export, or a component. The fix applies to that exact code; don't touch unrelated files.

2. Read the rule docs at <rule-url> for the full Insight explanation, then read the fix section at <fix-url>. Pick the pattern under "### Patterns" that matches the failing code, then read "### Gotchas" before editing — they list constraints that are easy to miss. Use the canonical imports and code shape from the page; don't improvise variations.

3. Apply the chosen pattern to the code identified in step 1.

## Error Type
Blocking Route

## Error Message
Route "/01-cookies-body": ...
    at Page (app/01-cookies-body/page.tsx:4:26)

## Code Frame
> 4 |   const c = await cookies();
    |                          ^
...

Next.js version: 16.3.0-canary.47 (Turbopack)
```

### Why

Old verbatim prompts repeated what the docs already say and never
anchored the fix to the actual error. The new template:

- Names the chosen fix (group badge + title) so the agent picks the
right pattern.
- Points at the rule docs AND the specific fix anchor, with explicit
instructions to read `### Patterns` and `### Gotchas`.
- Prepends the live error block (same shape the existing 'Copy error
info' button emits) so the agent has the failing file, line, code frame,
and stack inline — no need to re-fetch the dev overlay.

### Changes

- `FixCard` type: `prompt?: string` → `copyable?: boolean`
- `CopyPromptButton`: takes `title + group + link + generateErrorInfo`,
builds the prompt
- Refactored `generateErrorInfo` from `errors.tsx` into a shared
`utils/generate-error-info.ts` so the existing 'Copy error info' button
and the fix-card 'Copy AI prompt' button format the error block
identically
- 14 error MDX pages: every `prompt={...}` removed (36 strings)
- `instant-guidance-data.ts`: every `prompt: '...'` → `copyable: true`
(35 strings)
- Skill (`.agents/skills/insight-error-page/SKILL.md`): updated

### Pairs with

vercel/front#73087 — same template in the docs-site `<FixOption>`
(without the error block, since there's no live error on a docs page).

<!-- NEXT_JS_LLM_PR -->
2026-06-10 13:23:11 +02:00

289 lines
16 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"
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"
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"
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.
- 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.
- **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)