Back to Next Js

Next.js encountered URL data outside of Suspense

errors/instant-shell-url-data.mdx

16.3.012.8 KB
Original Source
<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 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.

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: ' <Details params={params} />' }, { 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 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.

Patterns

Pass searchParams to a suspended child

Don'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.

jsx
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
export async function Results({ searchParams }) {
  const { q } = await searchParams
  const widgets = await searchWidgets(q)
  return <WidgetList widgets={widgets} />
}

Learn more: searchParams.

Read params in the leaf that needs it

When 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.

jsx
import { Suspense } from 'react'
import { ProductDetails } from './product-details'

export default function Page({ params }) {
  return (
    <ProductLayout>
      <Suspense fallback={<DetailsSkeleton />}>
        <ProductDetails params={params} />
      </Suspense>
    </ProductLayout>
  )
}
jsx
export async function ProductDetails({ params }) {
  const { id } = await params
  const product = await getProduct(id)
  return <Details product={product} />
}

Learn more: Streaming.

Trade-off

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.

Gotchas

  • 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.
  • Making the route static with 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.
  • The 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.

Allow blocking route

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.

Patterns

Opt the page out

Add the export to the page that reads the URL data. Only that route blocks.

jsx
export const instant = false

export default async function Page({ searchParams }) {
  const { q } = await searchParams
  return <Results query={q} />
}

Learn more: Ensuring instant navigations.

Opt the layout out

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.

jsx
export const instant = false

export default function DashboardLayout({ children }) {
  return <DashboardShell>{children}</DashboardShell>
}

Learn more: Route segment instant config.

Use either pattern when:

  • The route genuinely needs the URL data high in the tree to decide what to render, so there's no shared part worth prefetching.
  • You're adopting Partial Prefetching incrementally and want to defer this route without changing how it renders today.

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.

Trade-off

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.

Gotchas

  • Setting instant to false opts only the segment that exports it out. Descendant segments are still validated by the global default.
  • Allowing the route to block does not disable Partial Prefetching or prefetching. It only exempts the segment from instant-navigation validation.
  • 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.

Verifying the fix

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.

Don't want this validation?

Instant-navigation validation runs by default in Cache Components apps and is what surfaces this error.

See Ensuring instant navigations for the full model.