From bb6f0c8d2f29754347db0ff28186dc89c128b6ca Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Mon, 1 Sep 2025 11:03:57 +0200 Subject: [PATCH 1/2] [Flight] Fix wrong missing key warning when static child is blocked (#34350) --- .../react-client/src/ReactFlightClient.js | 33 ++++++++-- .../__tests__/ReactFlightDOMBrowser-test.js | 60 +++++++++++++++++++ packages/react/src/ReactLazy.js | 2 + packages/react/src/jsx/ReactJSXElement.js | 16 +++++ 4 files changed, 105 insertions(+), 6 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index b966645623bbb..c583e4c84f6ef 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -1074,7 +1074,14 @@ function getTaskName(type: mixed): string { } } -function initializeElement(response: Response, element: any): void { +function initializeElement( + response: Response, + element: any, + lazyType: null | LazyComponent< + React$Element, + SomeChunk>, + >, +): void { if (!__DEV__) { return; } @@ -1141,6 +1148,18 @@ function initializeElement(response: Response, element: any): void { if (owner !== null) { initializeFakeStack(response, owner); } + + // In case the JSX runtime has validated the lazy type as a static child, we + // need to transfer this information to the element. + if ( + lazyType && + lazyType._store && + lazyType._store.validated && + !element._store.validated + ) { + element._store.validated = lazyType._store.validated; + } + // TODO: We should be freezing the element but currently, we might write into // _debugInfo later. We could move it into _store which remains mutable. Object.freeze(element.props); @@ -1230,7 +1249,7 @@ function createElement( handler.reason, ); if (__DEV__) { - initializeElement(response, element); + initializeElement(response, element, null); // Conceptually the error happened inside this Element but right before // it was rendered. We don't have a client side component to render but // we can add some DebugInfo to explain that this was conceptually a @@ -1258,16 +1277,17 @@ function createElement( createBlockedChunk(response); handler.value = element; handler.chunk = blockedChunk; + const lazyType = createLazyChunkWrapper(blockedChunk); if (__DEV__) { - /// After we have initialized any blocked references, initialize stack etc. - const init = initializeElement.bind(null, response, element); + // After we have initialized any blocked references, initialize stack etc. + const init = initializeElement.bind(null, response, element, lazyType); blockedChunk.then(init, init); } - return createLazyChunkWrapper(blockedChunk); + return lazyType; } } if (__DEV__) { - initializeElement(response, element); + initializeElement(response, element, null); } return element; @@ -1279,6 +1299,7 @@ function createLazyChunkWrapper( const lazyType: LazyComponent> = { $$typeof: REACT_LAZY_TYPE, _payload: chunk, + _store: {validated: 0}, _init: readChunk, }; if (__DEV__) { diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index cd546f61359dc..a49e268ebf040 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -2846,4 +2846,64 @@ describe('ReactFlightDOMBrowser', () => { expect(container.innerHTML).toBe('

Hi

'); }); + + it('should not have missing key warnings when a static child is blocked on debug info', async () => { + const ClientComponent = clientExports(function ClientComponent({element}) { + return ( +
+ Hi + {element} +
+ ); + }); + + let debugReadableStreamController; + + const debugReadableStream = new ReadableStream({ + start(controller) { + debugReadableStreamController = controller; + }, + }); + + const stream = await serverAct(() => + ReactServerDOMServer.renderToReadableStream( + Sebbie} />, + webpackMap, + { + debugChannel: { + writable: new WritableStream({ + write(chunk) { + debugReadableStreamController.enqueue(chunk); + }, + close() { + debugReadableStreamController.close(); + }, + }), + }, + }, + ), + ); + + function ClientRoot({response}) { + return use(response); + } + + const response = ReactServerDOMClient.createFromReadableStream(stream, { + debugChannel: {readable: createDelayedStream(debugReadableStream)}, + }); + + const container = document.createElement('div'); + const root = ReactDOMClient.createRoot(container); + + await act(() => { + root.render(); + }); + + // Wait for the debug info to be processed. + await act(() => {}); + + expect(container.innerHTML).toBe( + '
HiSebbie
', + ); + }); }); diff --git a/packages/react/src/ReactLazy.js b/packages/react/src/ReactLazy.js index 69b35b58cc8bd..3b7f97d6c352d 100644 --- a/packages/react/src/ReactLazy.js +++ b/packages/react/src/ReactLazy.js @@ -60,6 +60,8 @@ export type LazyComponent = { _payload: P, _init: (payload: P) => T, _debugInfo?: null | ReactDebugInfo, + // __DEV__ + _store?: {validated: 0 | 1 | 2, ...}, // 0: not validated, 1: validated, 2: force fail }; function lazyInitializer(payload: Payload): T { diff --git a/packages/react/src/jsx/ReactJSXElement.js b/packages/react/src/jsx/ReactJSXElement.js index cb475340c9c38..a77c4c3cdbf10 100644 --- a/packages/react/src/jsx/ReactJSXElement.js +++ b/packages/react/src/jsx/ReactJSXElement.js @@ -804,6 +804,14 @@ function validateChildKeys(node) { if (node._store) { node._store.validated = 1; } + } else if (isLazyType(node)) { + if (node._payload.status === 'fulfilled') { + if (isValidElement(node._payload.value) && node._payload.value._store) { + node._payload.value._store.validated = 1; + } + } else if (node._store) { + node._store.validated = 1; + } } } } @@ -822,3 +830,11 @@ export function isValidElement(object) { object.$$typeof === REACT_ELEMENT_TYPE ); } + +export function isLazyType(object) { + return ( + typeof object === 'object' && + object !== null && + object.$$typeof === REACT_LAZY_TYPE + ); +} From 1549bda33f0df963ae27a590b7191f3de99dad31 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Mon, 1 Sep 2025 12:13:05 +0200 Subject: [PATCH 2/2] [Flight] Only assign `_store` in dev mode when creating lazy types (#34354) Small follow-up to #34350. The `_store` property is now only assigned in development mode when creating lazy types. It also uses the `validated` value that was passed to `createElement`, if applicable. --- packages/react-client/src/ReactFlightClient.js | 12 +++++++----- packages/react/src/ReactLazy.js | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index c583e4c84f6ef..fc59a91fb2fac 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -1172,7 +1172,7 @@ function createElement( props: mixed, owner: ?ReactComponentInfo, // DEV-only stack: ?ReactStackTrace, // DEV-only - validated: number, // DEV-only + validated: 0 | 1 | 2, // DEV-only ): | React$Element | LazyComponent, SomeChunk>> { @@ -1268,7 +1268,7 @@ function createElement( } erroredChunk._debugInfo = [erroredComponent]; } - return createLazyChunkWrapper(erroredChunk); + return createLazyChunkWrapper(erroredChunk, validated); } if (handler.deps > 0) { // We have blocked references inside this Element but we can turn this into @@ -1277,7 +1277,7 @@ function createElement( createBlockedChunk(response); handler.value = element; handler.chunk = blockedChunk; - const lazyType = createLazyChunkWrapper(blockedChunk); + const lazyType = createLazyChunkWrapper(blockedChunk, validated); if (__DEV__) { // After we have initialized any blocked references, initialize stack etc. const init = initializeElement.bind(null, response, element, lazyType); @@ -1295,11 +1295,11 @@ function createElement( function createLazyChunkWrapper( chunk: SomeChunk, + validated: 0 | 1 | 2, // DEV-only ): LazyComponent> { const lazyType: LazyComponent> = { $$typeof: REACT_LAZY_TYPE, _payload: chunk, - _store: {validated: 0}, _init: readChunk, }; if (__DEV__) { @@ -1307,6 +1307,8 @@ function createLazyChunkWrapper( const chunkDebugInfo: ReactDebugInfo = chunk._debugInfo || (chunk._debugInfo = ([]: ReactDebugInfo)); lazyType._debugInfo = chunkDebugInfo; + // Initialize a store for key validation by the JSX runtime. + lazyType._store = {validated: validated}; } return lazyType; } @@ -2111,7 +2113,7 @@ function parseModelString( } // We create a React.lazy wrapper around any lazy values. // When passed into React, we'll know how to suspend on this. - return createLazyChunkWrapper(chunk); + return createLazyChunkWrapper(chunk, 0); } case '@': { // Promise diff --git a/packages/react/src/ReactLazy.js b/packages/react/src/ReactLazy.js index 3b7f97d6c352d..55b1690b7caea 100644 --- a/packages/react/src/ReactLazy.js +++ b/packages/react/src/ReactLazy.js @@ -59,8 +59,9 @@ export type LazyComponent = { $$typeof: symbol | number, _payload: P, _init: (payload: P) => T, - _debugInfo?: null | ReactDebugInfo, + // __DEV__ + _debugInfo?: null | ReactDebugInfo, _store?: {validated: 0 | 1 | 2, ...}, // 0: not validated, 1: validated, 2: force fail };