Skip to content

Commit a0a435d

Browse files
authored
[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.
1 parent 0a5e0b0 commit a0a435d

File tree

5 files changed

+104
-72
lines changed

5 files changed

+104
-72
lines changed

packages/react-devtools-shared/src/__tests__/store-test.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1916,11 +1916,9 @@ describe('Store', () => {
19161916
});
19171917

19181918
// 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]', () => {
19241922
function Example() {
19251923
return [<Child />];
19261924
}
@@ -1936,9 +1934,10 @@ describe('Store', () => {
19361934
);
19371935

19381936
expect(store).toMatchInlineSnapshot(`
1937+
✕ 1, ⚠ 0
19391938
[root]
19401939
▾ <Example>
1941-
<Child>
1940+
<Child>
19421941
`);
19431942
});
19441943

packages/react-dom/src/__tests__/ReactChildReconciler-test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ describe('ReactChildReconciler', () => {
129129
'duplicated and/or omitted — the behavior is unsupported and ' +
130130
'could change in a future version.\n' +
131131
' in div (at **)\n' +
132+
(gate(flags => flags.enableOwnerStacks) ? '' : ' in div (at **)\n') +
132133
' in Component (at **)\n' +
133134
(gate(flags => flags.enableOwnerStacks)
134135
? ''
@@ -190,6 +191,7 @@ describe('ReactChildReconciler', () => {
190191
'duplicated and/or omitted — the behavior is unsupported and ' +
191192
'could change in a future version.\n' +
192193
' in div (at **)\n' +
194+
(gate(flags => flags.enableOwnerStacks) ? '' : ' in div (at **)\n') +
193195
' in Component (at **)\n' +
194196
(gate(flags => flags.enableOwnerStacks)
195197
? ''

packages/react-dom/src/__tests__/ReactMultiChild-test.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -227,12 +227,13 @@ describe('ReactMultiChild', () => {
227227
'across updates. Non-unique keys may cause children to be ' +
228228
'duplicated and/or omitted — the behavior is unsupported and ' +
229229
'could change in a future version.\n' +
230-
' in div (at **)\n' +
231-
' in WrapperComponent (at **)\n' +
230+
' in div (at **)' +
232231
(gate(flags => flags.enableOwnerStacks)
233232
? ''
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 **)'),
236237
);
237238
});
238239

@@ -292,12 +293,13 @@ describe('ReactMultiChild', () => {
292293
'across updates. Non-unique keys may cause children to be ' +
293294
'duplicated and/or omitted — the behavior is unsupported and ' +
294295
'could change in a future version.\n' +
295-
' in div (at **)\n' +
296-
' in WrapperComponent (at **)\n' +
296+
' in div (at **)' +
297297
(gate(flags => flags.enableOwnerStacks)
298298
? ''
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 **)'),
301303
);
302304
});
303305
});

packages/react-reconciler/src/ReactChildFiber.js

Lines changed: 86 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,11 @@ let didWarnAboutGenerators;
9393
let ownerHasKeyUseWarning;
9494
let ownerHasFunctionTypeWarning;
9595
let ownerHasSymbolTypeWarning;
96-
let warnForMissingKey = (child: mixed, returnFiber: Fiber) => {};
96+
let warnForMissingKey = (
97+
returnFiber: Fiber,
98+
workInProgress: Fiber,
99+
child: mixed,
100+
) => {};
97101

98102
if (__DEV__) {
99103
didWarnAboutMaps = false;
@@ -108,7 +112,11 @@ if (__DEV__) {
108112
ownerHasFunctionTypeWarning = ({}: {[string]: boolean});
109113
ownerHasSymbolTypeWarning = ({}: {[string]: boolean});
110114

111-
warnForMissingKey = (child: mixed, returnFiber: Fiber) => {
115+
warnForMissingKey = (
116+
returnFiber: Fiber,
117+
workInProgress: Fiber,
118+
child: mixed,
119+
) => {
112120
if (child === null || typeof child !== 'object') {
113121
return;
114122
}
@@ -172,14 +180,7 @@ if (__DEV__) {
172180
}
173181
}
174182

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, () => {
183184
console.error(
184185
'Each child in a list should have a unique "key" prop.' +
185186
'%s%s See https://react.dev/link/warning-keys for more information.',
@@ -1034,9 +1035,10 @@ function createChildReconciler(
10341035
* Warns if there is a duplicate or missing key
10351036
*/
10361037
function warnOnInvalidKey(
1038+
returnFiber: Fiber,
1039+
workInProgress: Fiber,
10371040
child: mixed,
10381041
knownKeys: Set<string> | null,
1039-
returnFiber: Fiber,
10401042
): Set<string> | null {
10411043
if (__DEV__) {
10421044
if (typeof child !== 'object' || child === null) {
@@ -1045,7 +1047,7 @@ function createChildReconciler(
10451047
switch (child.$$typeof) {
10461048
case REACT_ELEMENT_TYPE:
10471049
case REACT_PORTAL_TYPE:
1048-
warnForMissingKey(child, returnFiber);
1050+
warnForMissingKey(returnFiber, workInProgress, child);
10491051
const key = child.key;
10501052
if (typeof key !== 'string') {
10511053
break;
@@ -1059,14 +1061,16 @@ function createChildReconciler(
10591061
knownKeys.add(key);
10601062
break;
10611063
}
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+
});
10701074
break;
10711075
case REACT_LAZY_TYPE: {
10721076
let resolvedChild;
@@ -1077,7 +1081,12 @@ function createChildReconciler(
10771081
const init = (child._init: any);
10781082
resolvedChild = init(payload);
10791083
}
1080-
warnOnInvalidKey(resolvedChild, knownKeys, returnFiber);
1084+
warnOnInvalidKey(
1085+
returnFiber,
1086+
workInProgress,
1087+
resolvedChild,
1088+
knownKeys,
1089+
);
10811090
break;
10821091
}
10831092
default:
@@ -1113,14 +1122,7 @@ function createChildReconciler(
11131122
// If you change this code, also update reconcileChildrenIterator() which
11141123
// uses the same algorithm.
11151124

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;
11241126

11251127
let resultingFirstChild: Fiber | null = null;
11261128
let previousNewFiber: Fiber | null = null;
@@ -1153,6 +1155,16 @@ function createChildReconciler(
11531155
}
11541156
break;
11551157
}
1158+
1159+
if (__DEV__) {
1160+
knownKeys = warnOnInvalidKey(
1161+
returnFiber,
1162+
newFiber,
1163+
newChildren[newIdx],
1164+
knownKeys,
1165+
);
1166+
}
1167+
11561168
if (shouldTrackSideEffects) {
11571169
if (oldFiber && newFiber.alternate === null) {
11581170
// We matched the slot, but we didn't reuse the existing fiber, so we
@@ -1198,6 +1210,14 @@ function createChildReconciler(
11981210
if (newFiber === null) {
11991211
continue;
12001212
}
1213+
if (__DEV__) {
1214+
knownKeys = warnOnInvalidKey(
1215+
returnFiber,
1216+
newFiber,
1217+
newChildren[newIdx],
1218+
knownKeys,
1219+
);
1220+
}
12011221
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
12021222
if (previousNewFiber === null) {
12031223
// TODO: Move out of the loop. This only happens for the first run.
@@ -1228,6 +1248,14 @@ function createChildReconciler(
12281248
debugInfo,
12291249
);
12301250
if (newFiber !== null) {
1251+
if (__DEV__) {
1252+
knownKeys = warnOnInvalidKey(
1253+
returnFiber,
1254+
newFiber,
1255+
newChildren[newIdx],
1256+
knownKeys,
1257+
);
1258+
}
12311259
if (shouldTrackSideEffects) {
12321260
if (newFiber.alternate !== null) {
12331261
// The new fiber is a work in progress, but if there exists a
@@ -1410,17 +1438,10 @@ function createChildReconciler(
14101438
let knownKeys: Set<string> | null = null;
14111439

14121440
let step = newChildren.next();
1413-
if (__DEV__) {
1414-
knownKeys = warnOnInvalidKey(step.value, knownKeys, returnFiber);
1415-
}
14161441
for (
14171442
;
14181443
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()
14241445
) {
14251446
if (oldFiber.index > newIdx) {
14261447
nextOldFiber = oldFiber;
@@ -1445,6 +1466,16 @@ function createChildReconciler(
14451466
}
14461467
break;
14471468
}
1469+
1470+
if (__DEV__) {
1471+
knownKeys = warnOnInvalidKey(
1472+
returnFiber,
1473+
newFiber,
1474+
step.value,
1475+
knownKeys,
1476+
);
1477+
}
1478+
14481479
if (shouldTrackSideEffects) {
14491480
if (oldFiber && newFiber.alternate === null) {
14501481
// We matched the slot, but we didn't reuse the existing fiber, so we
@@ -1480,19 +1511,19 @@ function createChildReconciler(
14801511
if (oldFiber === null) {
14811512
// If we don't have any more existing children we can choose a fast path
14821513
// 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()) {
14921515
const newFiber = createChild(returnFiber, step.value, lanes, debugInfo);
14931516
if (newFiber === null) {
14941517
continue;
14951518
}
1519+
if (__DEV__) {
1520+
knownKeys = warnOnInvalidKey(
1521+
returnFiber,
1522+
newFiber,
1523+
step.value,
1524+
knownKeys,
1525+
);
1526+
}
14961527
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
14971528
if (previousNewFiber === null) {
14981529
// TODO: Move out of the loop. This only happens for the first run.
@@ -1513,15 +1544,7 @@ function createChildReconciler(
15131544
const existingChildren = mapRemainingChildren(oldFiber);
15141545

15151546
// 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()) {
15251548
const newFiber = updateFromMap(
15261549
existingChildren,
15271550
returnFiber,
@@ -1531,6 +1554,14 @@ function createChildReconciler(
15311554
debugInfo,
15321555
);
15331556
if (newFiber !== null) {
1557+
if (__DEV__) {
1558+
knownKeys = warnOnInvalidKey(
1559+
returnFiber,
1560+
newFiber,
1561+
step.value,
1562+
knownKeys,
1563+
);
1564+
}
15341565
if (shouldTrackSideEffects) {
15351566
if (newFiber.alternate !== null) {
15361567
// The new fiber is a work in progress, but if there exists a

packages/react/src/__tests__/ReactJSXElementValidator-test.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -322,9 +322,7 @@ describe('ReactJSXElementValidator', () => {
322322
</>,
323323
);
324324
});
325-
}).toErrorDev('Encountered two children with the same key, `a`.', {
326-
withoutStack: true,
327-
});
325+
}).toErrorDev('Encountered two children with the same key, `a`.');
328326
});
329327

330328
it('does not call lazy initializers eagerly', () => {

0 commit comments

Comments
 (0)