Skip to content

Commit fbde80c

Browse files
committed
Implement duplicate key warnings
We keep known keys in a set in development. There is an annoying special case where we know we'll check it again because we break out of the loop early. One test in the tree hook regresses to the failing state because it checks that the tree hook works without a Set available, but we started using Set in this code. It is not essential and we can clean this up later when we decide how to deal with polyfills.
1 parent aa92f74 commit fbde80c

File tree

6 files changed

+116
-16
lines changed

6 files changed

+116
-16
lines changed

scripts/fiber/tests-failing.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ src/renderers/shared/__tests__/ReactPerf-test.js
6262

6363
src/renderers/shared/hooks/__tests__/ReactComponentTreeHook-test.js
6464
* can be retrieved by ID
65+
* works
6566

6667
src/renderers/shared/hooks/__tests__/ReactHostOperationHistoryHook-test.js
6768
* gets recorded during an update

scripts/fiber/tests-passing-except-dev.txt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ src/renderers/shared/hooks/__tests__/ReactComponentTreeHook-test.js
111111
* reports update counts
112112
* does not report top-level wrapper as a root
113113
* registers inlined text nodes
114-
* works
115114

116115
src/renderers/shared/hooks/__tests__/ReactHostOperationHistoryHook-test.js
117116
* gets recorded for host roots
@@ -133,10 +132,6 @@ src/renderers/shared/hooks/__tests__/ReactHostOperationHistoryHook-test.js
133132
* gets reported when a child is inserted
134133
* gets reported when a child is removed
135134

136-
src/renderers/shared/shared/__tests__/ReactChildReconciler-test.js
137-
* warns for duplicated keys
138-
* warns for duplicated keys with component stack info
139-
140135
src/renderers/shared/shared/__tests__/ReactComponentLifeCycle-test.js
141136
* should correctly determine if a component is mounted
142137
* should correctly determine if a null component is mounted
@@ -148,7 +143,6 @@ src/renderers/shared/shared/__tests__/ReactCompositeComponent-test.js
148143
* should disallow nested render calls
149144

150145
src/renderers/shared/shared/__tests__/ReactMultiChild-test.js
151-
* should warn for duplicated keys with component stack info
152146
* should warn for using maps as children with owner info
153147

154148
src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js

scripts/fiber/tests-passing.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,6 +1279,10 @@ src/renderers/shared/hooks/__tests__/ReactHostOperationHistoryHook-test.js
12791279
* gets ignored if the type has not changed
12801280
* gets ignored if new html is equal
12811281

1282+
src/renderers/shared/shared/__tests__/ReactChildReconciler-test.js
1283+
* warns for duplicated keys
1284+
* warns for duplicated keys with component stack info
1285+
12821286
src/renderers/shared/shared/__tests__/ReactComponent-test.js
12831287
* should throw when supplying a ref outside of render method
12841288
* should warn when children are mutated during render
@@ -1421,6 +1425,7 @@ src/renderers/shared/shared/__tests__/ReactMultiChild-test.js
14211425
* should replace children with different constructors
14221426
* should NOT replace children with different owners
14231427
* should replace children with different keys
1428+
* should warn for duplicated keys with component stack info
14241429
* should reorder bailed-out children
14251430

14261431
src/renderers/shared/shared/__tests__/ReactMultiChildReconcile-test.js

src/renderers/shared/fiber/ReactChildFiber.js

Lines changed: 108 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ var emptyObject = require('emptyObject');
3737
var getIteratorFn = require('getIteratorFn');
3838
var invariant = require('invariant');
3939

40+
if (__DEV__) {
41+
var ReactComponentTreeHook = require('ReactComponentTreeHook');
42+
var { getStackAddendumByFiber } = ReactComponentTreeHook;
43+
var warning = require('warning');
44+
}
45+
4046
const {
4147
cloneFiber,
4248
createFiberFromElement,
@@ -541,6 +547,44 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
541547
return null;
542548
}
543549

550+
function warnOnDuplicateKey(
551+
child : mixed,
552+
returnFiber : Fiber,
553+
knownKeys : Set<string>
554+
) {
555+
if (__DEV__) {
556+
if (child == null || typeof child !== 'object') {
557+
return;
558+
}
559+
switch (child.$$typeof) {
560+
// They have keys.
561+
case REACT_ELEMENT_TYPE:
562+
case REACT_COROUTINE_TYPE:
563+
case REACT_YIELD_TYPE:
564+
case REACT_PORTAL_TYPE:
565+
const key = child.key;
566+
if (typeof key !== 'string') {
567+
return;
568+
}
569+
if (knownKeys.has(key)) {
570+
warning(
571+
false,
572+
'Encountered two children with the same key, ' +
573+
'`%s`. Child keys must be unique; when two children share a key, ' +
574+
'only the first child will be used.%s',
575+
key,
576+
getStackAddendumByFiber(returnFiber)
577+
);
578+
} else {
579+
knownKeys.add(key);
580+
}
581+
return;
582+
default:
583+
return;
584+
}
585+
}
586+
}
587+
544588
function reconcileChildrenArray(
545589
returnFiber : Fiber,
546590
currentFirstChild : ?Fiber,
@@ -569,6 +613,8 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
569613
let resultingFirstChild : ?Fiber = null;
570614
let previousNewFiber : ?Fiber = null;
571615

616+
let knownKeysInDev = null;
617+
572618
let oldFiber = currentFirstChild;
573619
let lastPlacedIndex = 0;
574620
let newIdx = 0;
@@ -582,10 +628,15 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
582628
nextOldFiber = oldFiber.sibling;
583629
}
584630
}
631+
const newChild = newChildren[newIdx];
632+
if (__DEV__) {
633+
knownKeysInDev = knownKeysInDev || new Set();
634+
warnOnDuplicateKey(newChild, returnFiber, knownKeysInDev);
635+
}
585636
const newFiber = updateSlot(
586637
returnFiber,
587638
oldFiber,
588-
newChildren[newIdx],
639+
newChild,
589640
priority
590641
);
591642
if (!newFiber) {
@@ -596,6 +647,18 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
596647
if (!oldFiber) {
597648
oldFiber = nextOldFiber;
598649
}
650+
if (__DEV__) {
651+
if (
652+
knownKeysInDev != null &&
653+
newChild != null &&
654+
typeof newChild.key === 'string'
655+
) {
656+
// Since we break out of this loop without incrementing newIdx,
657+
// we will look at this child again in the next loop.
658+
// Forget we checked it to avoid a false positive for duplicate key.
659+
knownKeysInDev.delete(newChild.key);
660+
}
661+
}
599662
break;
600663
}
601664
if (shouldTrackSideEffects) {
@@ -630,9 +693,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
630693
// If we don't have any more existing children we can choose a fast path
631694
// since the rest will all be insertions.
632695
for (; newIdx < newChildren.length; newIdx++) {
696+
const newChild = newChildren[newIdx];
697+
if (__DEV__) {
698+
knownKeysInDev = knownKeysInDev || new Set();
699+
warnOnDuplicateKey(newChild, returnFiber, knownKeysInDev);
700+
}
633701
const newFiber = createChild(
634702
returnFiber,
635-
newChildren[newIdx],
703+
newChild,
636704
priority
637705
);
638706
if (!newFiber) {
@@ -655,11 +723,16 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
655723

656724
// Keep scanning and use the map to restore deleted items as moves.
657725
for (; newIdx < newChildren.length; newIdx++) {
726+
const newChild = newChildren[newIdx];
727+
if (__DEV__) {
728+
knownKeysInDev = knownKeysInDev || new Set();
729+
warnOnDuplicateKey(newChild, returnFiber, knownKeysInDev);
730+
}
658731
const newFiber = updateFromMap(
659732
existingChildren,
660733
returnFiber,
661734
newIdx,
662-
newChildren[newIdx],
735+
newChild,
663736
priority
664737
);
665738
if (newFiber) {
@@ -705,6 +778,8 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
705778
let resultingFirstChild : ?Fiber = null;
706779
let previousNewFiber : ?Fiber = null;
707780

781+
let knownKeysInDev = null;
782+
708783
let oldFiber = currentFirstChild;
709784
let lastPlacedIndex = 0;
710785
let newIdx = 0;
@@ -720,10 +795,15 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
720795
nextOldFiber = oldFiber.sibling;
721796
}
722797
}
798+
const newChild = step.value;
799+
if (__DEV__) {
800+
knownKeysInDev = knownKeysInDev || new Set();
801+
warnOnDuplicateKey(newChild, returnFiber, knownKeysInDev);
802+
}
723803
const newFiber = updateSlot(
724804
returnFiber,
725805
oldFiber,
726-
step.value,
806+
newChild,
727807
priority
728808
);
729809
if (!newFiber) {
@@ -734,6 +814,18 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
734814
if (!oldFiber) {
735815
oldFiber = nextOldFiber;
736816
}
817+
if (__DEV__) {
818+
if (
819+
knownKeysInDev != null &&
820+
newChild != null &&
821+
typeof newChild.key === 'string'
822+
) {
823+
// Since we break out of this loop without incrementing newIdx,
824+
// we will look at this child again in the next loop.
825+
// Forget we checked it to avoid a false positive for duplicate key.
826+
knownKeysInDev.delete(newChild.key);
827+
}
828+
}
737829
break;
738830
}
739831
if (shouldTrackSideEffects) {
@@ -768,9 +860,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
768860
// If we don't have any more existing children we can choose a fast path
769861
// since the rest will all be insertions.
770862
for (; !step.done; newIdx++, step = newChildren.next()) {
863+
const newChild = step.value;
864+
if (__DEV__) {
865+
knownKeysInDev = knownKeysInDev || new Set();
866+
warnOnDuplicateKey(newChild, returnFiber, knownKeysInDev);
867+
}
771868
const newFiber = createChild(
772869
returnFiber,
773-
step.value,
870+
newChild,
774871
priority
775872
);
776873
if (!newFiber) {
@@ -793,11 +890,16 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
793890

794891
// Keep scanning and use the map to restore deleted items as moves.
795892
for (; !step.done; newIdx++, step = newChildren.next()) {
893+
const newChild = step.value;
894+
if (__DEV__) {
895+
knownKeysInDev = knownKeysInDev || new Set();
896+
warnOnDuplicateKey(newChild, returnFiber, knownKeysInDev);
897+
}
796898
const newFiber = updateFromMap(
797899
existingChildren,
798900
returnFiber,
799901
newIdx,
800-
step.value,
902+
newChild,
801903
priority
802904
);
803905
if (newFiber) {

src/renderers/shared/shared/__tests__/ReactChildReconciler-test.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,7 @@ describe('ReactChildReconciler', () => {
7070
ReactTestUtils.renderIntoDocument(<GrandParent />);
7171

7272
expectDev(console.error.calls.count()).toBe(1);
73-
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
74-
'Warning: flattenChildren(...): ' +
73+
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toContain(
7574
'Encountered two children with the same key, `1`. ' +
7675
'Child keys must be unique; when two children share a key, ' +
7776
'only the first child will be used.\n' +

src/renderers/shared/shared/__tests__/ReactMultiChild-test.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,7 @@ describe('ReactMultiChild', () => {
186186
);
187187

188188
expectDev(console.error.calls.count()).toBe(1);
189-
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
190-
'Warning: flattenChildren(...): ' +
189+
expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toContain(
191190
'Encountered two children with the same key, `1`. ' +
192191
'Child keys must be unique; when two children share a key, ' +
193192
'only the first child will be used.\n' +

0 commit comments

Comments
 (0)