errors/instant-shell-url-data.mdx
During a client-side navigation, a Server Component read params or searchParams outside of a <Suspense> boundary. With Partial Prefetching enabled, Next.js extracts one App Shell from the route ahead of the click, so every link to it reuses the same prefetch instead of fetching a fresh one per URL.
The params and searchParams props are URL data: they're specific to a single URL, so reading them outside a <Suspense> boundary ties the App Shell to one link. Next.js can no longer reuse it across links, and navigations to this route may not be instant.
The check runs when you load the route and when you navigate to it. The App Shell itself is only used for client-side navigations: the initial load has its own validation against the route's static shell, where the same read surfaces as runtime data during prerendering. For URL data read through client hooks like useSearchParams, see URL data in a Client Component outside of Suspense.
Choose this fix when the URL-specific content can render after the navigation. A <Suspense> boundary keeps the read out of the App Shell, so every link still shares the same prefetch and only the wrapped region streams in after the navigation.
searchParams to a suspended childDon't await params or searchParams at the top of the route. Pass the promise to a child that reads it inside its own boundary, so the rest of the route stays in the shared prefetch.
import { Suspense } from 'react'
import { Results } from './results'
export default function Page({ searchParams }) {
return (
<DashboardShell>
<DashboardHeader />
<Suspense fallback={<ResultsSkeleton />}>
<Results searchParams={searchParams} />
</Suspense>
</DashboardShell>
)
}
export async function Results({ searchParams }) {
const { q } = await searchParams
const widgets = await searchWidgets(q)
return <WidgetList widgets={widgets} />
}
Learn more: searchParams.
params in the leaf that needs itWhen only a small piece of UI depends on the route param, move the read down to that leaf and wrap it. Everything above stays in the shared prefetch.
import { Suspense } from 'react'
import { ProductDetails } from './product-details'
export default function Page({ params }) {
return (
<ProductLayout>
<Suspense fallback={<DetailsSkeleton />}>
<ProductDetails params={params} />
</Suspense>
</ProductLayout>
)
}
export async function ProductDetails({ params }) {
const { id } = await params
const product = await getProduct(id)
return <Details product={product} />
}
Learn more: Streaming.
The shared parts of the route are prefetched, and the URL-dependent region streams in after navigation, so the user sees a fallback for that region. Design the fallback so it approximates the final layout. A generic spinner causes a layout shift when content arrives. See minimizing layout shift.
cookies() and headers() don't trigger this error, even outside <Suspense>. They vary per session, not per link, so the App Shell stays reusable across links. The initial load's static shell may still need them behind <Suspense>, and that requirement surfaces separately as runtime data during prerendering.generateStaticParams does not resolve this error. A static param is still specific to one URL, so it can't be part of a prefetch shared across links.params and searchParams props are promises. Passing the promise down without awaiting it keeps the rest of the route in the shared prefetch. Awaiting it above the boundary pulls the URL data back in.Choose this fix when the route genuinely can't provide a shared App Shell — it needs the URL data high in the tree to decide what to render — and you accept that navigations to it won't be instant. Setting instant to false marks the segment as allowed to block: it renders per navigation instead of reusing a shared prefetch.
Add the export to the page that reads the URL data. Only that route blocks.
export const instant = false
export default async function Page({ searchParams }) {
const { q } = await searchParams
return <Results query={q} />
}
Learn more: Ensuring instant navigations.
When a shared layout reads the URL data, 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 this error, allowing the route to block is rarely the right answer. The route reads a small piece of URL data, and a <Suspense> boundary around that read keeps the rest of the route in the shared prefetch. Choose Wrap in or move into Suspense when feasible.
Navigations to this route are not instant. Without an App Shell, it renders per navigation instead of reusing a shared prefetch. Use this only when the route genuinely can't provide one.
instant to false opts only the segment that exports it out. Descendant segments are still validated by the global default.instant = false allows the route to block for all instant-navigation checks, not only this one. That includes runtime data during prerendering errors and unrendered segment warnings for the route.After applying a fix, navigate to the route and confirm the insight no longer appears in the dev overlay and the page immediately paints meaningful UI, with any <Suspense> fallbacks covering only the regions that stream in. A <Suspense> boundary around the whole page body can pass validation with an empty shell, which defeats the point of an instant navigation. Depending on your validation level, the insight may only surface in development.
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