@samitouri / QOS-React / commits / bb6f0c8d2f

[Flight] Fix wrong missing key warning when static child is blocked (#34350)

Hendrik Liebau committed Sep 1, 2025 at 11:03 UTC bb6f0c8d2f29754347db0ff28186dc89c128b6ca
4 files changed +105 -6
packages/react-client/src/ReactFlightClient.js
+27 -6
@@ -1074,7 +1074,14 @@ function getTaskName(type: mixed): string {
1074 }
1075 }
1076
1077 -function initializeElement(response: Response, element: any): void {
1077 +function initializeElement(
1078 + response: Response,
1079 + element: any,
1080 + lazyType: null | LazyComponent<
1081 + React$Element<any>,
1082 + SomeChunk<React$Element<any>>,
1083 + >,
1084 +): void {
1085 if (!__DEV__) {
1086 return;
1087 }
@@ -1141,6 +1148,18 @@ function initializeElement(response: Response, element: any): void {
1148 if (owner !== null) {
1149 initializeFakeStack(response, owner);
1150 }
1151 +
1152 + // In case the JSX runtime has validated the lazy type as a static child, we
1153 + // need to transfer this information to the element.
1154 + if (
1155 + lazyType &&
1156 + lazyType._store &&
1157 + lazyType._store.validated &&
1158 + !element._store.validated
1159 + ) {
1160 + element._store.validated = lazyType._store.validated;
1161 + }
1162 +
1163 // TODO: We should be freezing the element but currently, we might write into
1164 // _debugInfo later. We could move it into _store which remains mutable.
1165 Object.freeze(element.props);
@@ -1230,7 +1249,7 @@ function createElement(
1249 handler.reason,
1250 );
1251 if (__DEV__) {
1233 - initializeElement(response, element);
1252 + initializeElement(response, element, null);
1253 // Conceptually the error happened inside this Element but right before
1254 // it was rendered. We don't have a client side component to render but
1255 // we can add some DebugInfo to explain that this was conceptually a
@@ -1258,16 +1277,17 @@ function createElement(
1277 createBlockedChunk(response);
1278 handler.value = element;
1279 handler.chunk = blockedChunk;
1280 + const lazyType = createLazyChunkWrapper(blockedChunk);
1281 if (__DEV__) {
1262 - /// After we have initialized any blocked references, initialize stack etc.
1263 - const init = initializeElement.bind(null, response, element);
1282 + // After we have initialized any blocked references, initialize stack etc.
1283 + const init = initializeElement.bind(null, response, element, lazyType);
1284 blockedChunk.then(init, init);
1285 }
1266 - return createLazyChunkWrapper(blockedChunk);
1286 + return lazyType;
1287 }
1288 }
1289 if (__DEV__) {
1270 - initializeElement(response, element);
1290 + initializeElement(response, element, null);
1291 }
1292
1293 return element;
@@ -1279,6 +1299,7 @@ function createLazyChunkWrapper<T>(
1299 const lazyType: LazyComponent<T, SomeChunk<T>> = {
1300 $$typeof: REACT_LAZY_TYPE,
1301 _payload: chunk,
1302 + _store: {validated: 0},
1303 _init: readChunk,
1304 };
1305 if (__DEV__) {
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js
+60
@@ -2846,4 +2846,64 @@ describe('ReactFlightDOMBrowser', () => {
2846
2847 expect(container.innerHTML).toBe('<p>Hi</p>');
2848 });
2849 +
2850 + it('should not have missing key warnings when a static child is blocked on debug info', async () => {
2851 + const ClientComponent = clientExports(function ClientComponent({element}) {
2852 + return (
2853 + <div>
2854 + <span>Hi</span>
2855 + {element}
2856 + </div>
2857 + );
2858 + });
2859 +
2860 + let debugReadableStreamController;
2861 +
2862 + const debugReadableStream = new ReadableStream({
2863 + start(controller) {
2864 + debugReadableStreamController = controller;
2865 + },
2866 + });
2867 +
2868 + const stream = await serverAct(() =>
2869 + ReactServerDOMServer.renderToReadableStream(
2870 + <ClientComponent element={<span>Sebbie</span>} />,
2871 + webpackMap,
2872 + {
2873 + debugChannel: {
2874 + writable: new WritableStream({
2875 + write(chunk) {
2876 + debugReadableStreamController.enqueue(chunk);
2877 + },
2878 + close() {
2879 + debugReadableStreamController.close();
2880 + },
2881 + }),
2882 + },
2883 + },
2884 + ),
2885 + );
2886 +
2887 + function ClientRoot({response}) {
2888 + return use(response);
2889 + }
2890 +
2891 + const response = ReactServerDOMClient.createFromReadableStream(stream, {
2892 + debugChannel: {readable: createDelayedStream(debugReadableStream)},
2893 + });
2894 +
2895 + const container = document.createElement('div');
2896 + const root = ReactDOMClient.createRoot(container);
2897 +
2898 + await act(() => {
2899 + root.render(<ClientRoot response={response} />);
2900 + });
2901 +
2902 + // Wait for the debug info to be processed.
2903 + await act(() => {});
2904 +
2905 + expect(container.innerHTML).toBe(
2906 + '<div><span>Hi</span><span>Sebbie</span></div>',
2907 + );
2908 + });
2909 });
packages/react/src/ReactLazy.js
+2
@@ -60,6 +60,8 @@ export type LazyComponent<T, P> = {
60 _payload: P,
61 _init: (payload: P) => T,
62 _debugInfo?: null | ReactDebugInfo,
63 + // __DEV__
64 + _store?: {validated: 0 | 1 | 2, ...}, // 0: not validated, 1: validated, 2: force fail
65 };
66
67 function lazyInitializer<T>(payload: Payload<T>): T {
packages/react/src/jsx/ReactJSXElement.js
+16
@@ -804,6 +804,14 @@ function validateChildKeys(node) {
804 if (node._store) {
805 node._store.validated = 1;
806 }
807 + } else if (isLazyType(node)) {
808 + if (node._payload.status === 'fulfilled') {
809 + if (isValidElement(node._payload.value) && node._payload.value._store) {
810 + node._payload.value._store.validated = 1;
811 + }
812 + } else if (node._store) {
813 + node._store.validated = 1;
814 + }
815 }
816 }
817 }
@@ -822,3 +830,11 @@ export function isValidElement(object) {
830 object.$$typeof === REACT_ELEMENT_TYPE
831 );
832 }
833 +
834 +export function isLazyType(object) {
835 + return (
836 + typeof object === 'object' &&
837 + object !== null &&
838 + object.$$typeof === REACT_LAZY_TYPE
839 + );
840 +}