[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
+}