@samitouri / QOS-React / commits / a0a435d68f

[Fiber] Track the Real Fiber for Key Warnings (#29791)

This refactors key warning to happen inline after we've matched a Fiber. I didn't want to do that originally because it was riskier. But it turns out to be straightforward enough. This lets us use that Fiber as the source of the warning which matters to DevTools because then DevTools can associate it with the right component after it mounts. We can also associate the duplicate key warning with this Fiber. That way we'll get the callsite with the duplicate key on the stack and can associate this warning with the child that had the duplicate. I kept the forked DevTools tests because the warning now is counted on the Child instead of the Parent (18 behavior). However, this won't be released in 19.0.0 so I only test this in whatever the next version is. Doesn't seem worth it to have a test for just the 19.0.0 behavior.

Sebastian Markbåge committed Jun 7, 2024 at 13:38 UTC a0a435d68f2e4e506faef4e763d13cbeb2e819c8
5 files changed +104 -72
packages/react-devtools-shared/src/__tests__/store-test.js
+5 -6
@@ -1916,11 +1916,9 @@ describe('Store', () => {
1916 });
1917
1918 // In React 19, JSX warnings were moved into the renderer - https://github.com/facebook/react/pull/29088
1919 - // When the error is emitted, the source fiber of this error is not yet mounted
1920 - // So DevTools can't connect the error and the fiber
1921 - // TODO(hoxyq): update RDT to keep track of such fibers
1922 - // @reactVersion >= 19.0
1923 - it('from react get counted [React >= 19]', () => {
1919 + // The warning is moved to the Child instead of the Parent.
1920 + // @reactVersion >= 19.0.1
1921 + it('from react get counted [React >= 19.0.1]', () => {
1922 function Example() {
1923 return [<Child />];
1924 }
@@ -1936,9 +1934,10 @@ describe('Store', () => {
1934 );
1935
1936 expect(store).toMatchInlineSnapshot(`
1937 + ✕ 1, ⚠ 0
1938 [root]
1939 ▾ <Example>
1941 - <Child>
1940 + <Child> ✕
1941 `);
1942 });
1943
packages/react-dom/src/__tests__/ReactChildReconciler-test.js
+2
@@ -129,6 +129,7 @@ describe('ReactChildReconciler', () => {
129 'duplicated and/or omitted — the behavior is unsupported and ' +
130 'could change in a future version.\n' +
131 ' in div (at **)\n' +
132 + (gate(flags => flags.enableOwnerStacks) ? '' : ' in div (at **)\n') +
133 ' in Component (at **)\n' +
134 (gate(flags => flags.enableOwnerStacks)
135 ? ''
@@ -190,6 +191,7 @@ describe('ReactChildReconciler', () => {
191 'duplicated and/or omitted — the behavior is unsupported and ' +
192 'could change in a future version.\n' +
193 ' in div (at **)\n' +
194 + (gate(flags => flags.enableOwnerStacks) ? '' : ' in div (at **)\n') +
195 ' in Component (at **)\n' +
196 (gate(flags => flags.enableOwnerStacks)
197 ? ''
packages/react-dom/src/__tests__/ReactMultiChild-test.js
+10 -8
@@ -227,12 +227,13 @@ describe('ReactMultiChild', () => {
227 'across updates. Non-unique keys may cause children to be ' +
228 'duplicated and/or omitted — the behavior is unsupported and ' +
229 'could change in a future version.\n' +
230 - ' in div (at **)\n' +
231 - ' in WrapperComponent (at **)\n' +
230 + ' in div (at **)' +
231 (gate(flags => flags.enableOwnerStacks)
232 ? ''
234 - : ' in div (at **)\n') +
235 - ' in Parent (at **)',
233 + : '\n in div (at **)' +
234 + '\n in WrapperComponent (at **)' +
235 + '\n in div (at **)' +
236 + '\n in Parent (at **)'),
237 );
238 });
239
@@ -292,12 +293,13 @@ describe('ReactMultiChild', () => {
293 'across updates. Non-unique keys may cause children to be ' +
294 'duplicated and/or omitted — the behavior is unsupported and ' +
295 'could change in a future version.\n' +
295 - ' in div (at **)\n' +
296 - ' in WrapperComponent (at **)\n' +
296 + ' in div (at **)' +
297 (gate(flags => flags.enableOwnerStacks)
298 ? ''
299 - : ' in div (at **)\n') +
300 - ' in Parent (at **)',
299 + : '\n in div (at **)' +
300 + '\n in WrapperComponent (at **)' +
301 + '\n in div (at **)' +
302 + '\n in Parent (at **)'),
303 );
304 });
305 });
packages/react-reconciler/src/ReactChildFiber.js
+86 -55
@@ -93,7 +93,11 @@ let didWarnAboutGenerators;
93 let ownerHasKeyUseWarning;
94 let ownerHasFunctionTypeWarning;
95 let ownerHasSymbolTypeWarning;
96 -let warnForMissingKey = (child: mixed, returnFiber: Fiber) => {};
96 +let warnForMissingKey = (
97 + returnFiber: Fiber,
98 + workInProgress: Fiber,
99 + child: mixed,
100 +) => {};
101
102 if (__DEV__) {
103 didWarnAboutMaps = false;
@@ -108,7 +112,11 @@ if (__DEV__) {
112 ownerHasFunctionTypeWarning = ({}: {[string]: boolean});
113 ownerHasSymbolTypeWarning = ({}: {[string]: boolean});
114
111 - warnForMissingKey = (child: mixed, returnFiber: Fiber) => {
115 + warnForMissingKey = (
116 + returnFiber: Fiber,
117 + workInProgress: Fiber,
118 + child: mixed,
119 + ) => {
120 if (child === null || typeof child !== 'object') {
121 return;
122 }
@@ -172,14 +180,7 @@ if (__DEV__) {
180 }
181 }
182
175 - // We create a fake Fiber for the child to log the stack trace from.
176 - // TODO: Refactor the warnForMissingKey calls to happen after fiber creation
177 - // so that we can get access to the fiber that will eventually be created.
178 - // That way the log can show up associated with the right instance in DevTools.
179 - const fiber = createFiberFromElement((child: any), returnFiber.mode, 0);
180 - fiber.return = returnFiber;
181 -
182 - runWithFiberInDEV(fiber, () => {
183 + runWithFiberInDEV(workInProgress, () => {
184 console.error(
185 'Each child in a list should have a unique "key" prop.' +
186 '%s%s See https://react.dev/link/warning-keys for more information.',
@@ -1034,9 +1035,10 @@ function createChildReconciler(
1035 * Warns if there is a duplicate or missing key
1036 */
1037 function warnOnInvalidKey(
1038 + returnFiber: Fiber,
1039 + workInProgress: Fiber,
1040 child: mixed,
1041 knownKeys: Set<string> | null,
1039 - returnFiber: Fiber,
1042 ): Set<string> | null {
1043 if (__DEV__) {
1044 if (typeof child !== 'object' || child === null) {
@@ -1045,7 +1047,7 @@ function createChildReconciler(
1047 switch (child.$$typeof) {
1048 case REACT_ELEMENT_TYPE:
1049 case REACT_PORTAL_TYPE:
1048 - warnForMissingKey(child, returnFiber);
1050 + warnForMissingKey(returnFiber, workInProgress, child);
1051 const key = child.key;
1052 if (typeof key !== 'string') {
1053 break;
@@ -1059,14 +1061,16 @@ function createChildReconciler(
1061 knownKeys.add(key);
1062 break;
1063 }
1062 - console.error(
1063 - 'Encountered two children with the same key, `%s`. ' +
1064 - 'Keys should be unique so that components maintain their identity ' +
1065 - 'across updates. Non-unique keys may cause children to be ' +
1066 - 'duplicated and/or omitted — the behavior is unsupported and ' +
1067 - 'could change in a future version.',
1068 - key,
1069 - );
1064 + runWithFiberInDEV(workInProgress, () => {
1065 + console.error(
1066 + 'Encountered two children with the same key, `%s`. ' +
1067 + 'Keys should be unique so that components maintain their identity ' +
1068 + 'across updates. Non-unique keys may cause children to be ' +
1069 + 'duplicated and/or omitted — the behavior is unsupported and ' +
1070 + 'could change in a future version.',
1071 + key,
1072 + );
1073 + });
1074 break;
1075 case REACT_LAZY_TYPE: {
1076 let resolvedChild;
@@ -1077,7 +1081,12 @@ function createChildReconciler(
1081 const init = (child._init: any);
1082 resolvedChild = init(payload);
1083 }
1080 - warnOnInvalidKey(resolvedChild, knownKeys, returnFiber);
1084 + warnOnInvalidKey(
1085 + returnFiber,
1086 + workInProgress,
1087 + resolvedChild,
1088 + knownKeys,
1089 + );
1090 break;
1091 }
1092 default:
@@ -1113,14 +1122,7 @@ function createChildReconciler(
1122 // If you change this code, also update reconcileChildrenIterator() which
1123 // uses the same algorithm.
1124
1116 - if (__DEV__) {
1117 - // First, validate keys.
1118 - let knownKeys: Set<string> | null = null;
1119 - for (let i = 0; i < newChildren.length; i++) {
1120 - const child = newChildren[i];
1121 - knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber);
1122 - }
1123 - }
1125 + let knownKeys: Set<string> | null = null;
1126
1127 let resultingFirstChild: Fiber | null = null;
1128 let previousNewFiber: Fiber | null = null;
@@ -1153,6 +1155,16 @@ function createChildReconciler(
1155 }
1156 break;
1157 }
1158 +
1159 + if (__DEV__) {
1160 + knownKeys = warnOnInvalidKey(
1161 + returnFiber,
1162 + newFiber,
1163 + newChildren[newIdx],
1164 + knownKeys,
1165 + );
1166 + }
1167 +
1168 if (shouldTrackSideEffects) {
1169 if (oldFiber && newFiber.alternate === null) {
1170 // We matched the slot, but we didn't reuse the existing fiber, so we
@@ -1198,6 +1210,14 @@ function createChildReconciler(
1210 if (newFiber === null) {
1211 continue;
1212 }
1213 + if (__DEV__) {
1214 + knownKeys = warnOnInvalidKey(
1215 + returnFiber,
1216 + newFiber,
1217 + newChildren[newIdx],
1218 + knownKeys,
1219 + );
1220 + }
1221 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
1222 if (previousNewFiber === null) {
1223 // TODO: Move out of the loop. This only happens for the first run.
@@ -1228,6 +1248,14 @@ function createChildReconciler(
1248 debugInfo,
1249 );
1250 if (newFiber !== null) {
1251 + if (__DEV__) {
1252 + knownKeys = warnOnInvalidKey(
1253 + returnFiber,
1254 + newFiber,
1255 + newChildren[newIdx],
1256 + knownKeys,
1257 + );
1258 + }
1259 if (shouldTrackSideEffects) {
1260 if (newFiber.alternate !== null) {
1261 // The new fiber is a work in progress, but if there exists a
@@ -1410,17 +1438,10 @@ function createChildReconciler(
1438 let knownKeys: Set<string> | null = null;
1439
1440 let step = newChildren.next();
1413 - if (__DEV__) {
1414 - knownKeys = warnOnInvalidKey(step.value, knownKeys, returnFiber);
1415 - }
1441 for (
1442 ;
1443 oldFiber !== null && !step.done;
1419 - newIdx++,
1420 - step = newChildren.next(),
1421 - knownKeys = __DEV__
1422 - ? warnOnInvalidKey(step.value, knownKeys, returnFiber)
1423 - : null
1444 + newIdx++, step = newChildren.next()
1445 ) {
1446 if (oldFiber.index > newIdx) {
1447 nextOldFiber = oldFiber;
@@ -1445,6 +1466,16 @@ function createChildReconciler(
1466 }
1467 break;
1468 }
1469 +
1470 + if (__DEV__) {
1471 + knownKeys = warnOnInvalidKey(
1472 + returnFiber,
1473 + newFiber,
1474 + step.value,
1475 + knownKeys,
1476 + );
1477 + }
1478 +
1479 if (shouldTrackSideEffects) {
1480 if (oldFiber && newFiber.alternate === null) {
1481 // We matched the slot, but we didn't reuse the existing fiber, so we
@@ -1480,19 +1511,19 @@ function createChildReconciler(
1511 if (oldFiber === null) {
1512 // If we don't have any more existing children we can choose a fast path
1513 // since the rest will all be insertions.
1483 - for (
1484 - ;
1485 - !step.done;
1486 - newIdx++,
1487 - step = newChildren.next(),
1488 - knownKeys = __DEV__
1489 - ? warnOnInvalidKey(step.value, knownKeys, returnFiber)
1490 - : null
1491 - ) {
1514 + for (; !step.done; newIdx++, step = newChildren.next()) {
1515 const newFiber = createChild(returnFiber, step.value, lanes, debugInfo);
1516 if (newFiber === null) {
1517 continue;
1518 }
1519 + if (__DEV__) {
1520 + knownKeys = warnOnInvalidKey(
1521 + returnFiber,
1522 + newFiber,
1523 + step.value,
1524 + knownKeys,
1525 + );
1526 + }
1527 lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
1528 if (previousNewFiber === null) {
1529 // TODO: Move out of the loop. This only happens for the first run.
@@ -1513,15 +1544,7 @@ function createChildReconciler(
1544 const existingChildren = mapRemainingChildren(oldFiber);
1545
1546 // Keep scanning and use the map to restore deleted items as moves.
1516 - for (
1517 - ;
1518 - !step.done;
1519 - newIdx++,
1520 - step = newChildren.next(),
1521 - knownKeys = __DEV__
1522 - ? warnOnInvalidKey(step.value, knownKeys, returnFiber)
1523 - : null
1524 - ) {
1547 + for (; !step.done; newIdx++, step = newChildren.next()) {
1548 const newFiber = updateFromMap(
1549 existingChildren,
1550 returnFiber,
@@ -1531,6 +1554,14 @@ function createChildReconciler(
1554 debugInfo,
1555 );
1556 if (newFiber !== null) {
1557 + if (__DEV__) {
1558 + knownKeys = warnOnInvalidKey(
1559 + returnFiber,
1560 + newFiber,
1561 + step.value,
1562 + knownKeys,
1563 + );
1564 + }
1565 if (shouldTrackSideEffects) {
1566 if (newFiber.alternate !== null) {
1567 // The new fiber is a work in progress, but if there exists a
packages/react/src/__tests__/ReactJSXElementValidator-test.js
+1 -3
@@ -322,9 +322,7 @@ describe('ReactJSXElementValidator', () => {
322 </>,
323 );
324 });
325 - }).toErrorDev('Encountered two children with the same key, `a`.', {
326 - withoutStack: true,
327 - });
325 + }).toErrorDev('Encountered two children with the same key, `a`.');
326 });
327
328 it('does not call lazy initializers eagerly', () => {