diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index b966645623bbb..fc59a91fb2fac 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); @@ -1153,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>> { @@ -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 @@ -1249,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 @@ -1258,16 +1277,17 @@ function createElement( createBlockedChunk(response); handler.value = element; handler.chunk = 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); + // 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; @@ -1275,6 +1295,7 @@ function createElement( function createLazyChunkWrapper( chunk: SomeChunk, + validated: 0 | 1 | 2, // DEV-only ): LazyComponent> { const lazyType: LazyComponent> = { $$typeof: REACT_LAZY_TYPE, @@ -1286,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; } @@ -2090,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-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..55b1690b7caea 100644 --- a/packages/react/src/ReactLazy.js +++ b/packages/react/src/ReactLazy.js @@ -59,7 +59,10 @@ export type LazyComponent = { $$typeof: symbol | number, _payload: P, _init: (payload: P) => T, + + // __DEV__ _debugInfo?: null | ReactDebugInfo, + _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 + ); +}