Files
Alessio Gravili 1ecd7dd1f2 feat(next): prevent admin panel errors when cacheComponents is enabled (#16020)
Fixes https://github.com/payloadcms/payload/issues/8897, addresses
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

These are all caused by Next.js's `cacheComponents` and likely cannot be
fixed from our side.

### 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](https://nextjs.org/docs/app/api-reference/file-conventions/loading#status-codes).

### 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](https://github.com/vercel/next.js/issues/86577) 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](https://github.com/vercel/next.js/issues/86577#issuecomment-3801284197)),
we cannot _guarantee_ full admin panel compatibility beyond the initial
error suppression this PR provides.
2026-03-26 13:17:41 -04:00
..