Back to Next Js

Next.js encountered URL data in a Client Component outside of Suspense

errors/blocking-prerender-client-hook.mdx

16.3.015.6 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 prerendering, a Client Component called a navigation hook (usePathname, useParams, useSearchParams, useSelectedLayoutSegment, or useSelectedLayoutSegments) outside of a <Suspense> boundary. With Cache Components 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.

The useSearchParams hook 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 and is rendered per-request.

Server-side request-bound reads (cookies(), headers()) have different fixes. See Next.js encountered runtime data during prerendering. For unstable values like Math.random() in Client Components, see Next.js encountered the unstable value Math.random() in a Client Component.

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: ' <SidebarNav />' }, { 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 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>. Next.js prerenders the fallback and streams the real value in when it's available.

jsx
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
'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

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
import { Nav } from './nav'

export default function Layout({ children }) {
  return (
    <div>
      <Nav />
      {children}
    </div>
  )
}
jsx
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
'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, Creating an active link component with useSelectedLayoutSegment

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
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
'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 cause a layout shift. See minimizing layout shift.

Gotchas

  • Place the <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(), Date.now(), crypto.randomUUID(), or fetch() inside it. Each one raises a separate Cache Components error during prerendering.
  • Do not pass {children} through in the fallback. Child pages may include dynamic reads (for example, /_not-found calling cookies() or 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 to set the active attribute from location.pathname so the correct link is styled on the first paint.
  • usePathname 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.

Allow blocking route

Choose this fix when the route renders per-request and there's no useful static shell. Setting 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
export const instant = false

export default function Page() {
  return <Dashboard />
}

Learn more: Ensuring instant navigations.

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 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 needs request-time data high in the tree to decide what to render, so there is no meaningful 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> boundary around that read keeps the rest of the route prerendered. Choose 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 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. 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> 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, 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.