From c0cd77d28678b9d36dc4f9707ac636b99156a017 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Sat, 18 Oct 2025 17:13:04 -0700 Subject: [PATCH] [Cache Components] Reenable static shell validation on client nav in Dev When we disabled static shell validation on client navigations in dev we also turned it off for HMR updates as well because HMR for server components is modeled as a client nav to the current page. We could add complex tracking of whether the last navigation was an initial page load and then conditionally do the validation on HMR update but we already intend to add prefetch validation for client navigations soon and when we do that we will be using a more efficient technique that does not render the RSC response more than once. We'll live with the potential dev client nav perf regression for now until we land the longer term picture which will reenable the validation for HMR too making it harder to miss these warnings when you end up editing files. --- .../next/src/server/app-render/app-render.tsx | 28 +++++++++++++++++-- .../cache-components-dev-errors.test.ts | 27 ++++++++++++------ 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 6725d7ba7217e..bc4ac7723f44d 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -716,7 +716,8 @@ async function generateDynamicFlightRenderResultWithStagesInDev( req: BaseNextRequest, ctx: AppRenderContext, initialRequestStore: RequestStore, - createRequestStore: (() => RequestStore) | undefined + createRequestStore: (() => RequestStore) | undefined, + tree: LoaderTree ): Promise { const { htmlRequestId, @@ -746,6 +747,8 @@ async function generateDynamicFlightRenderResultWithStagesInDev( onFlightDataRenderError ) + const [resolveValidation, validationOutlet] = createValidationOutlet() + const getPayload = async (requestStore: RequestStore) => { const payload: RSCPayload & RSCPayloadDevProperties = await workUnitAsyncStorage.run( @@ -763,6 +766,8 @@ async function generateDynamicFlightRenderResultWithStagesInDev( }) } + payload._validation = validationOutlet + return payload } @@ -822,6 +827,24 @@ async function generateDynamicFlightRenderResultWithStagesInDev( setReactDebugChannel(debugChannel.clientSide, htmlRequestId, requestId) } + const devValidatingFallbackParams = + getRequestMeta(req, 'devValidatingFallbackParams') || null + + // TODO(restart-on-cache-miss): + // This can probably be optimized to do less work, + // because we've already made sure that we have warm caches. + consoleAsyncStorage.run( + { dim: true }, + spawnDynamicValidationInDev, + resolveValidation, + tree, + ctx, + false, + ctx.clientReferenceManifest, + initialRequestStore, + devValidatingFallbackParams + ) + return new FlightRenderResult(stream, { fetchMetrics: workStore.fetchMetrics, }) @@ -2016,7 +2039,8 @@ async function renderToHTMLOrFlightImpl( req, ctx, requestStore, - createRequestStore + createRequestStore, + loaderTree ) } else { return generateDynamicFlightRenderResult(req, ctx, requestStore) diff --git a/test/development/app-dir/cache-components-dev-errors/cache-components-dev-errors.test.ts b/test/development/app-dir/cache-components-dev-errors/cache-components-dev-errors.test.ts index 0028e5acf0c50..335421dd1b401 100644 --- a/test/development/app-dir/cache-components-dev-errors/cache-components-dev-errors.test.ts +++ b/test/development/app-dir/cache-components-dev-errors/cache-components-dev-errors.test.ts @@ -1,11 +1,6 @@ import stripAnsi from 'strip-ansi' import { nextTestSetup } from 'e2e-utils' -import { - assertNoRedbox, - assertNoErrorToast, - hasErrorToast, - retry, -} from 'next-test-utils' +import { assertNoRedbox, hasErrorToast, retry } from 'next-test-utils' import { createSandbox } from 'development-sandbox' import { outdent } from 'outdent' @@ -37,7 +32,7 @@ describe('Cache Components Dev Errors', () => { `) }) - it('should not show a red box error on client navigations', async () => { + it('should show a red box error on client navigations', async () => { const browser = await next.browser('/no-error') await retry(async () => { @@ -45,10 +40,24 @@ describe('Cache Components Dev Errors', () => { }) await browser.elementByCss("[href='/error']").click() - await assertNoErrorToast(browser) + // TODO: React should not include the anon stack in the Owner Stack. + await expect(browser).toDisplayCollapsedRedbox(` + { + "description": "Route "/error" used \`Math.random()\` before accessing either uncached data (e.g. \`fetch()\`) or Request data (e.g. \`cookies()\`, \`headers()\`, \`connection()\`, and \`searchParams\`). Accessing random values synchronously in a Server Component requires reading one of these data sources first. Alternatively, consider moving this expression into a Client Component or Cache Component. See more info here: https://nextjs.org/docs/messages/next-prerender-random", + "environmentLabel": "Server", + "label": "Console Error", + "source": "app/error/page.tsx (2:23) @ Page + > 2 | const random = Math.random() + | ^", + "stack": [ + "Page app/error/page.tsx (2:23)", + "Page ", + "LogSafely ", + ], + } + `) await browser.loadPage(`${next.url}/error`) - // TODO: React should not include the anon stack in the Owner Stack. await expect(browser).toDisplayCollapsedRedbox(` {