mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
## What? Adds the Instant Insight + rule page for the Partial Prefetching shell validation (Janka's #95151): with `partialPrefetching` on, a route that reads `params`/`searchParams` outside `<Suspense>` blocks the extraction of the per-route **App Shell** that every link to the route shares. Surfaced as an Instant insight (not a redbox), with two fix cards: Stream (wrap in `<Suspense>`) · Allow blocking route (`instant = false`). The insight fires both at link-prefetch time and during a client-side navigation, so it covers a `<Link prefetch>` into the route as well as a plain client navigation to it. Demos: - [99-link-params-gsp](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/99-link-params-gsp) — `<Link prefetch>` to a `generateStaticParams` route that awaits `params` unguarded. - [117-subnav-params](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/117-subnav-params) — client-side navigation into a `[slug]` route that reads `params` outside `<Suspense>`. - [118-subnav-search-params](https://error-messages-overhaul-ibsl.labs.vercel.dev/scenario/118-subnav-search-params) — client-side navigation into a route that reads `searchParams` outside `<Suspense>`. Follow-up: #95389 documents the route-side audit in the Adopting Partial Prefetching guide (draft, to be coordinated). ## How? - Family-wide polish across all 16 insight pages while aligning the new one: consistent "Verifying the fix" wording, normalized `Learn more:` link texts (target title for guides, bare code name for API references), "App Shell" used consistently, CLS guidance reframed as "minimizing layout shift", prose cleanups (no semicolons, no code-initial sentences), and the insight-error-page skill updated to match. - New `errors/instant-shell-url-data.mdx` rule page, structured like the rest of the insight family (`blocking-prerender-client-hook`, `instant-link-prefetch-partial`); added to every page's Related Insights. New **URL data** glossary entry ("varies per link, not per session"), linked from the page. - `createLinkBodyErrorInNavigation` re-pointed to the page and classified as a navigation-time insight (`inNavigation` → Instant tab) using the shared `linkCards` fix cards. - Common insight format for the message: headline "Next.js encountered URL data outside of Suspense", consequence "may prevent the navigation from being instant, leading to a slower user experience" ("may", since boundaries or a runtime prefetch can still keep the navigation instant), and the two fixes phrased as `[stream] Provide a placeholder with <Suspense fallback={...}> around the data access` and `[block] Set export const instant = false to allow a blocking route`. The shell mechanics are explained on the docs page rather than in the headline. - Second fix reframed from "Disable validation" (group `ignore`) to **Allow blocking route** (group `block`), matching the `blocking-prerender-*` family: `instant = false` marks the segment as allowed to block rather than opting the route out of validation. - Metadata/viewport variants: only the headline term is aligned ("link data" → "URL data") plus a small grammar fix; framing, fix cards, and docs URLs stay on the existing `-metadata-runtime`/`-viewport-runtime` pages unchanged. - `errors.json` regenerated append-only (codes 1407-1409), and the docs opt-out wording updated to the new silence-free phrasing. - e2e snapshots updated for all four configs (dev/start × default/partial-prefetching); unit tests for the matcher, cards, classification, and `isInstantNavigationError`. <!-- NEXT_JS_LLM_PR -->
281 lines
17 KiB
Plaintext
281 lines
17 KiB
Plaintext
---
|
|
title: Next.js encountered runtime data during prerendering or a navigation
|
|
kind: insight
|
|
---
|
|
|
|
<div
|
|
style={{
|
|
padding: '1.25rem 1.5rem',
|
|
border: '1px solid var(--ds-gray-400)',
|
|
borderRadius: '12px',
|
|
background: 'var(--ds-background-200)',
|
|
margin: '1.5rem 0 2rem',
|
|
fontSize: '0.95rem',
|
|
lineHeight: '1.6',
|
|
}}
|
|
>
|
|
This Insight is part of the [Instant
|
|
Navigations](https://nextjs.org/blog/next-16-3-instant-navigations) feature
|
|
introduced in Next.js 16.3. If you're new to it, start with the [Ensuring
|
|
instant
|
|
navigations](https://preview.nextjs.org/docs/app/guides/instant-navigation)
|
|
guide for an overview of what instant navigations are and how Next.js
|
|
validates them, then come back here for the specific fix.
|
|
</div>
|
|
|
|
During [prerendering](https://preview.nextjs.org/docs/app/glossary#prerendering), [`cookies()`](https://preview.nextjs.org/docs/app/api-reference/functions/cookies), [`headers()`](https://preview.nextjs.org/docs/app/api-reference/functions/headers), [`params`](https://preview.nextjs.org/docs/app/api-reference/file-conventions/page#params-optional), or [`searchParams`](https://preview.nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional) was read outside of [`<Suspense>`](https://react.dev/reference/react/Suspense). With [Cache Components](https://preview.nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents) enabled, Next.js can't prerender any part of the tree that depends on a per-request value, so navigations to this route block instead of being [instant](https://preview.nextjs.org/docs/app/guides/instant-navigation).
|
|
|
|
Uncached data accesses ([`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch), database calls, [`await connection()`](https://preview.nextjs.org/docs/app/api-reference/functions/connection)) have different fixes. See [Next.js encountered uncached data during prerendering](/docs/messages/blocking-prerender-dynamic).
|
|
|
|
This error can also appear during a client-side navigation when the data access sits inside a [`<Suspense>`](https://react.dev/reference/react/Suspense) boundary from a parent layout but that boundary is too high. It wraps the entire segment instead of only the dynamic part, so the navigation still blocks. Push the boundary closer to the data access so the rest of the segment stays in the [static shell](https://preview.nextjs.org/docs/app/glossary#static-shell). See [Choosing where to place the boundary](#choosing-where-to-place-the-boundary).
|
|
|
|
## Ways to fix this
|
|
|
|
<FixCardGrid>
|
|
<FixCard
|
|
group="stream"
|
|
title="Wrap in or move into Suspense"
|
|
href="#wrap-in-or-move-into-suspense"
|
|
snippets={[
|
|
{ text: '<Suspense fallback={…}>', highlight: true },
|
|
{ text: ' <DataChild />' },
|
|
{ text: '</Suspense>', highlight: true },
|
|
]}
|
|
/>
|
|
<FixCard
|
|
group="block"
|
|
title="Allow blocking route"
|
|
href="#allow-blocking-route"
|
|
snippets={[
|
|
{ text: '// page.tsx or layout.tsx' },
|
|
{ text: 'export const instant = false', highlight: true },
|
|
]}
|
|
/>
|
|
</FixCardGrid>
|
|
|
|
## Wrap in or move into Suspense
|
|
|
|
Choose this fix when the value really is per-request, but the page has parts that don't depend on it. A [`<Suspense>`](https://react.dev/reference/react/Suspense) boundary lets the static shell ship instantly while the dynamic region [streams](https://preview.nextjs.org/docs/app/glossary#streaming) in once the request value resolves.
|
|
|
|
### Patterns
|
|
|
|
#### Wrap the existing component in place
|
|
|
|
Keep the component that reads the runtime API intact and add a [`<Suspense>`](https://react.dev/reference/react/Suspense) boundary around its usage in the parent.
|
|
|
|
```jsx filename="app/dashboard/page.js"
|
|
import { Suspense } from 'react'
|
|
import { UserHeader } from './user-header'
|
|
import { HeaderSkeleton } from './header-skeleton'
|
|
|
|
export default function Page() {
|
|
return (
|
|
<DashboardShell>
|
|
<Suspense fallback={<HeaderSkeleton />}>
|
|
<UserHeader />
|
|
</Suspense>
|
|
<CachedStats />
|
|
</DashboardShell>
|
|
)
|
|
}
|
|
```
|
|
|
|
Learn more: [Streaming](https://preview.nextjs.org/docs/app/guides/streaming).
|
|
|
|
#### Push the access down to the leaf
|
|
|
|
When the value is read at the top of the tree but only consumed by a small piece of UI, move the read down. The parent stays prerenderable and only the leaf needs a boundary.
|
|
|
|
```jsx filename="app/dashboard/page.js"
|
|
import { Suspense } from 'react'
|
|
|
|
export default function Page() {
|
|
return (
|
|
<DashboardShell>
|
|
<Suspense fallback={<HeaderSkeleton />}>
|
|
<UserHeader />
|
|
</Suspense>
|
|
<CachedStats />
|
|
</DashboardShell>
|
|
)
|
|
}
|
|
```
|
|
|
|
```jsx filename="app/dashboard/user-header.js"
|
|
import { cookies } from 'next/headers'
|
|
|
|
export async function UserHeader() {
|
|
const session = (await cookies()).get('session')
|
|
return <header>Signed in as {session?.value}</header>
|
|
}
|
|
```
|
|
|
|
Learn more: [Streaming](https://preview.nextjs.org/docs/app/guides/streaming).
|
|
|
|
#### Pass `searchParams` without awaiting
|
|
|
|
When the consumer is a child component, pass the promise down instead of awaiting it at the top. The child wraps its own consumption in [`<Suspense>`](https://react.dev/reference/react/Suspense), which keeps the parent prerenderable.
|
|
|
|
```jsx filename="app/dashboard/page.js"
|
|
import { Suspense } from 'react'
|
|
import { Results } from './results'
|
|
|
|
export default function Page({ searchParams }) {
|
|
return (
|
|
<DashboardShell>
|
|
<DashboardHeader />
|
|
<Suspense fallback={<ResultsSkeleton />}>
|
|
<Results searchParams={searchParams} />
|
|
</Suspense>
|
|
</DashboardShell>
|
|
)
|
|
}
|
|
```
|
|
|
|
```jsx filename="app/dashboard/results.js"
|
|
export async function Results({ searchParams }) {
|
|
const { q } = await searchParams
|
|
const widgets = await searchWidgets(q)
|
|
return <WidgetList widgets={widgets} />
|
|
}
|
|
```
|
|
|
|
Learn more: [`searchParams`](https://preview.nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional).
|
|
|
|
#### Forward `searchParams` as a promise chain
|
|
|
|
A variant of the previous pattern when you want to derive a value without awaiting at the top. Treat [`searchParams`](https://preview.nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional) as a promise and `.then()` it to project the shape the child needs.
|
|
|
|
```jsx filename="app/dashboard/map/page.js"
|
|
export default function Page({ searchParams }) {
|
|
const coords = searchParams.then((sp) => ({
|
|
lat: Number(sp.lat),
|
|
lng: Number(sp.lng),
|
|
}))
|
|
return <Map coords={coords} />
|
|
}
|
|
```
|
|
|
|
Learn more: [`searchParams`](https://preview.nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional).
|
|
|
|
#### Use `loading.js` for the whole segment
|
|
|
|
When every component in the segment reads the same request value and there's nothing static to render above it, a [`loading.js`](https://preview.nextjs.org/docs/app/api-reference/file-conventions/loading) file in the segment is the shorthand. Next.js wraps `{children}` of the layout in `<Suspense>` automatically.
|
|
|
|
```jsx filename="app/dashboard/loading.js"
|
|
export default function Loading() {
|
|
return <DashboardSkeleton />
|
|
}
|
|
```
|
|
|
|
> **Good to know**: A `loading.js` file wraps the segment's `{children}` in one Suspense boundary. Parent layouts above it still prerender, but everything inside the segment sits behind the fallback. If page-level content could be prerendered (a static intro, a known title), use explicit `<Suspense>` boundaries inside `page.js` around only the dynamic parts.
|
|
|
|
Learn more: [`loading.js` and instant loading states](https://preview.nextjs.org/docs/app/api-reference/file-conventions/loading).
|
|
|
|
### Trade-off
|
|
|
|
The shell ships immediately, but the user sees a loading state for the streamed region on every request. Design the fallback so it approximates the final layout. A generic spinner causes a layout shift when content arrives. See [minimizing layout shift](https://preview.nextjs.org/docs/app/guides/streaming#cls-cumulative-layout-shift).
|
|
|
|
### Choosing where to place the boundary
|
|
|
|
The location of the boundary controls what the user sees during the navigation:
|
|
|
|
- A high boundary (around the whole page) gives one loading state for everything. Less work to set up, but the user loses context about where they were going.
|
|
- A low boundary (around the specific component that reads the runtime API) keeps surrounding content visible and only shows a fallback for the per-request part. Preferred when the surrounding shell has cached content.
|
|
|
|
A useful rule: **push the boundary as low as possible** while keeping the fallback meaningful. The cached content above the boundary becomes part of the [static shell](https://preview.nextjs.org/docs/app/glossary#static-shell) on navigation. Wrapping individual pieces or wrapping the whole page in one boundary stream the same way, but a lower boundary keeps more prerendered content visible during the navigation. See [Maximizing the static shell](https://preview.nextjs.org/docs/app/getting-started/caching#streaming-uncached-data) for the canonical pattern.
|
|
|
|
### Gotchas
|
|
|
|
- The fallback must be deterministic. Calling [`Math.random()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random) or [`Date.now()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now) inside the fallback raises a separate [Cache Components](https://preview.nextjs.org/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()`](https://preview.nextjs.org/docs/app/api-reference/functions/cookies) or [`headers()`](https://preview.nextjs.org/docs/app/api-reference/functions/headers)) that propagate into what should be a static fallback. Render a placeholder that doesn't include `{children}`.
|
|
- If the failing route is `/_not-found` and you don't have a `not-found.tsx` file, the read is in the root layout. `/_not-found` is a real prerendered route that inherits the root layout, so a `cookies()` or `headers()` read there fails on the synthetic route too. Run `next build --debug-prerender` to confirm the originating file, and fix it at the layout, not by adding a `not-found.tsx`.
|
|
- Boundary placement affects client navigations between sibling routes differently than initial page loads. Validation surfaces this in the dev server and at build time. See [Ensuring instant navigations](https://preview.nextjs.org/docs/app/guides/instant-navigation) for the full model.
|
|
- The function returned by [`cookies()`](https://preview.nextjs.org/docs/app/api-reference/functions/cookies) and [`headers()`](https://preview.nextjs.org/docs/app/api-reference/functions/headers) is async. Make sure the component reading them is async too, and `await` the call.
|
|
- The `params` and `searchParams` props are also async promises. Treat them like any other awaited value when deciding where the boundary goes.
|
|
- Root-element attributes (`<html lang>`, `<html dir>`, `<html data-theme>`) can't be wrapped in `<Suspense>`. You can't suspend the document root, and a boundary inside `<html>` still leaves the attribute itself server-cookie-dependent. Move the read to a pre-paint client script per [Preventing flash before hydration](https://preview.nextjs.org/docs/app/guides/preventing-flash-before-hydration) and add `suppressHydrationWarning` on `<html>` so React doesn't flag the script's mutation as a mismatch.
|
|
|
|
## Allow blocking route
|
|
|
|
Choose this fix when the route renders per-request and there's no useful static shell. Setting [`instant`](https://preview.nextjs.org/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 async function Page() {
|
|
const session = (await cookies()).get('session')
|
|
return <Dashboard session={session?.value} />
|
|
}
|
|
```
|
|
|
|
Learn more: [Ensuring instant navigations](https://preview.nextjs.org/docs/app/guides/instant-navigation).
|
|
|
|
#### Opt the layout out
|
|
|
|
When the shared layout itself can't ship instantly (it reads [`cookies()`](https://preview.nextjs.org/docs/app/api-reference/functions/cookies) or [`headers()`](https://preview.nextjs.org/docs/app/api-reference/functions/headers) of its own), set [`instant`](https://preview.nextjs.org/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](https://preview.nextjs.org/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 (for example auth, tenant, or other gating in a layout), so there is no meaningful [static shell](https://preview.nextjs.org/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.
|
|
|
|
Don't use this to dismiss the error. Choose [Wrap in or move into Suspense](#wrap-in-or-move-into-suspense) when 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`](https://preview.nextjs.org/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](https://preview.nextjs.org/docs/app/glossary#prerendering). The route still prerenders if it can. It only disables instant-navigation validation for the route.
|
|
|
|
## Verifying the fix
|
|
|
|
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>`](https://react.dev/reference/react/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`](https://preview.nextjs.org/docs/app/api-reference/cli/next#next-dev-options), the error overlay points at the failing component with file paths and line numbers. When working from a build instead, the default [`next build`](https://preview.nextjs.org/docs/app/api-reference/cli/next#next-build-options) 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.
|
|
|
|
## Don't want this validation?
|
|
|
|
Instant-navigation validation runs by default in [Cache Components](https://preview.nextjs.org/docs/app/api-reference/config/next-config-js/cacheComponents) apps and is what surfaces this error.
|
|
|
|
- **One segment**: add [`export const instant = false`](https://preview.nextjs.org/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`](https://preview.nextjs.org/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](https://preview.nextjs.org/docs/app/guides/instant-navigation) for the full model.
|
|
|
|
## Related Insights
|
|
|
|
- [Uncached data during prerendering](/docs/messages/blocking-prerender-dynamic)
|
|
- [URL data in a Client Component outside of Suspense](/docs/messages/blocking-prerender-client-hook)
|
|
- [Runtime data in `generateMetadata()`](/docs/messages/blocking-prerender-metadata-runtime)
|
|
- [Uncached data in `generateMetadata()`](/docs/messages/blocking-prerender-metadata-dynamic)
|
|
- [Runtime data in `generateViewport()`](/docs/messages/blocking-prerender-viewport-runtime)
|
|
- [Uncached data in `generateViewport()`](/docs/messages/blocking-prerender-viewport-dynamic)
|
|
- [`Math.random()` while prerendering](/docs/messages/blocking-prerender-random)
|
|
- [`Math.random()` in a Client Component](/docs/messages/blocking-prerender-random-client)
|
|
- [`Date.now()` while prerendering](/docs/messages/blocking-prerender-current-time)
|
|
- [`Date.now()` in a Client Component](/docs/messages/blocking-prerender-current-time-client)
|
|
- [Crypto APIs while prerendering](/docs/messages/blocking-prerender-crypto)
|
|
- [Crypto APIs in a Client Component](/docs/messages/blocking-prerender-crypto-client)
|
|
- [Dynamic data during prefetching](/docs/messages/instant-link-prefetch-partial)
|
|
- [URL data outside of Suspense](/docs/messages/instant-shell-url-data)
|
|
- [Unrendered segment](/docs/messages/instant-unrendered-segment)
|