nextjs - ✅(Solved) Fix Docs: How to intentionally block in Cache Components? [1 pull requests, 24 comments, 12 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
vercel/next.js#86739Fetched 2026-04-08 02:09:30
View on GitHub
Comments
24
Participants
12
Timeline
85
Reactions
16
Author
Assignees
Timeline (top)
subscribed ×38commented ×24mentioned ×13labeled ×4

Error Message

We've all seen this error:

Root Cause

(They're using Next but I don't think they're using CC; I'm just using this example because it's illustrative)

Fix Action

Workaround

I've recently learned that it's possible to work around the problem like this:

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  // Note: you might not want to copy this pattern (see explanation below)
  return (
    <Suspense fallback={null}>
      <html lang="en">
        <body>
          ...
        </body>
      </html>
    </Suspense>
  );
}

Apparently, if Suspense is placed above the <html> tag, it will both shut up the Cache Components "runtime access" warning and prevent an incomplete first frame from showing up in the browser. This wasn't obvious though, and it's a bit nuclear — now I don't get any warnings about runtime access at all, not even for more nested pages where it would be valuable.

What I really want to express is: this specific bit of data (getting the session + profile info) is something I want to block the UI on and I do not want to place <Suspense> boundaries around, however I'd still appreciate warnings in other places.

Hope this is helpful!

PR fix notes

PR #16020: feat(next): prevent admin panel errors when cacheComponents is enabled

Description (problem / solution / changelog)

Fixes https://github.com/payloadcms/payload/issues/8897, partially addresses cache component support (https://github.com/payloadcms/payload/discussions/14460)

Adds initial support for Next.js cacheComponents so users who enable it for their frontend don't get errors from the Payload admin panel. This PR addresses the obvious breakage but does not guarantee full compatibility - see the "Known Limitations" section below.

When cacheComponents is enabled in next.config, Next.js throws "Data that blocks navigation was accessed outside of <Suspense>" errors because the admin layout reads cookies, headers, and does auth queries at the top level. This prevents users from enabling cacheComponents at all if Payload is in the same Next.js app.

The fix has two parts. First, withPayload now detects cacheComponents in the Next.js config and sets a PAYLOAD_CACHE_COMPONENTS_ENABLED env var. Second, RootLayout reads that env var and conditionally wraps its content in <Suspense fallback={null}> above the <html> tag, which suppresses the errors. When cacheComponents is not enabled, the Suspense is not used at all and behavior is identical to before.

Known Limitations

Page flash on hard refresh

When cacheComponents is enabled, hard refresh shows a brief gray flash before the admin panel appears. Without cacheComponents there is no flash. There is no per-route opt-out for this behavior. Related issue: https://github.com/vercel/next.js/issues/86739

HTTP status codes (404 returns 200)

With cacheComponents, notFound() returns HTTP 200 instead of 404. This happens because the Suspense boundary above <html> causes Next.js to commit response headers (with status 200) before notFound() runs inside the suspended content. The not-found UI still renders correctly - only the HTTP status code is wrong. This is a documented Next.js streaming limitation.

DOM accumulation breaks Playwright selectors

When cacheComponents is enabled, Next.js wraps route segments in React's <Activity> component, keeping up to 3 previously visited pages in the DOM with display: none !important instead of unmounting them. This means Playwright selectors like page.locator('#field-title') resolve to multiple elements (the visible one and hidden copies from cached pages), causing strict mode violations. This is a known issue affecting all Next.js apps using cacheComponents with Playwright.

Because of this, we cannot reliably run our e2e test suite with cacheComponents enabled. Adapting the test suite would require rewriting a large number of selectors across hundreds of tests - most of our e2e tests use page.locator() with ID selectors, which would all break when Activity duplicates the DOM. Until the Next.js team provides a per-route opt-out for Activity (which they are actively exploring), we cannot guarantee full admin panel compatibility beyond the initial error suppression this PR provides.


Changed files

  • .github/workflows/main.yml (modified, +3/-2)
  • .github/workflows/utilities/e2e-matrix.ts (modified, +5/-1)
  • docs/getting-started/installation.mdx (modified, +8/-0)
  • next.config.mjs (modified, +2/-1)
  • packages/next/src/layouts/Root/index.tsx (modified, +36/-9)
  • packages/next/src/views/API/index.client.tsx (modified, +9/-1)
  • packages/next/src/withPayload/withPayload.js (modified, +4/-0)
  • test/next.config.mjs (modified, +1/-0)
  • test/playwright.config.ts (modified, +1/-0)

Code Example

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  // Note: you might not want to copy this pattern (see explanation below)
  return (
    <Suspense fallback={null}>
      <html lang="en">
        <body>
          ...
        </body>
      </html>
    </Suspense>
  );
}
RAW_BUFFERClick to expand / collapse

What is the documentation issue?

We've all seen this error:

Runtime data such as cookies(), headers(), params, or searchParams was accessed outside of <Suspense>.

First, I want to say that I really like that Cache Components forces to find where you block unnecessarily. Given the reputation of App Router as being somehow slow, I think forcing to add Suspense boundaries does help in the sense that developers can see that it's their code is blocking, and have an opportunity to fix it.

However, this seems to make some common patterns impossible to express without warnings.

In particular, I'm interested in the pattern where the app's UI depends on whether you're logged in or not. A logged-in and logged-out states are significantly different in their nature so a <Suspense> boundary is not always adequate or appropriate, and it could not be determined before reading cookies() either.

Consider this UI from a website I use:

https://github.com/user-attachments/assets/560ab953-724e-4fab-abd4-78e2c320a5cc

(They're using Next but I don't think they're using CC; I'm just using this example because it's illustrative)

Notice a placeholder in the piece of UI corresponding to the user avi. I personally hate when websites do that.

This kind of UX feels like a relic of the jamstack era when the client actually was unable to determine whether you're logged in or not and so it had to display a placeholder while figuring out. Many people who use Next don't actually want to do jamstack and are fine with running some code dynamically.

The problem is that this "jamstacky" UX pattern seems forced upon the developers by the Cache Components approach, even when there's no actual perf upside for the user. Reading cookies() and then deciding to display the avi (or not) does not necessarily need any expensive data roundtrip justifying sending a shell without it. In my opinion, for the kind of apps I built, the top bar (including the avi) should just be a part of the initial shell.

Workaround

I've recently learned that it's possible to work around the problem like this:

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  // Note: you might not want to copy this pattern (see explanation below)
  return (
    <Suspense fallback={null}>
      <html lang="en">
        <body>
          ...
        </body>
      </html>
    </Suspense>
  );
}

Apparently, if Suspense is placed above the <html> tag, it will both shut up the Cache Components "runtime access" warning and prevent an incomplete first frame from showing up in the browser. This wasn't obvious though, and it's a bit nuclear — now I don't get any warnings about runtime access at all, not even for more nested pages where it would be valuable.

What I really want to express is: this specific bit of data (getting the session + profile info) is something I want to block the UI on and I do not want to place <Suspense> boundaries around, however I'd still appreciate warnings in other places.

Hope this is helpful!

Is there any context that might help us understand?

Does the docs page already exist? Please link to it.

No response

extent analysis

TL;DR

To resolve the "Runtime data accessed outside of <Suspense>" error, consider wrapping the <html> tag with a <Suspense> component with a fallback prop set to null, but be aware this may suppress other valuable warnings.

Guidance

  • The error occurs because Cache Components requires runtime data access within a <Suspense> boundary, which may not be suitable for all use cases, such as when the UI depends on login state.
  • To mitigate this, you can use the provided workaround of wrapping the <html> tag with a <Suspense> component, but be cautious as this may disable other warnings about runtime access.
  • Consider the implications of blocking the UI on specific data, such as session and profile information, and weigh the trade-offs of using <Suspense> boundaries in your application.
  • Evaluate whether the workaround is suitable for your use case, or if alternative approaches, such as reorganizing your component tree or using different data fetching strategies, might be more effective.

Example

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <Suspense fallback={null}>
      <html lang="en">
        <body>
          {/* your app content */}
        </body>
      </html>
    </Suspense>
  );
}

Notes

The provided workaround may not be ideal for all situations, as it suppresses all runtime access warnings. Further discussion or exploration of alternative solutions may be necessary to find a more suitable approach.

Recommendation

Apply the workaround with caution, as it may have unintended consequences, such as disabling valuable warnings about runtime access in other parts of the application.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

nextjs - ✅(Solved) Fix Docs: How to intentionally block in Cache Components? [1 pull requests, 24 comments, 12 participants]