Skip to content

Commit 5724657

Browse files
committed
Perform duplicate key validation in a separate pass
This avoids complicating the main loops and will be easier to port if that code gets forked into more special cases. I also added tests specifically for iterables since they weren't being tested. The tree hook test that used to fail now passes because we create a Set lazily and so it doesn't trigger that code path. We'll revisit this when we decide how to polyfill.
1 parent 5719458 commit 5724657

6 files changed

Lines changed: 209 additions & 97 deletions

File tree

scripts/fiber/tests-failing.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ 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
6665

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ 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
114115

115116
src/renderers/shared/hooks/__tests__/ReactHostOperationHistoryHook-test.js
116117
* gets recorded for host roots

scripts/fiber/tests-passing.txt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1280,8 +1280,10 @@ src/renderers/shared/hooks/__tests__/ReactHostOperationHistoryHook-test.js
12801280
* gets ignored if new html is equal
12811281

12821282
src/renderers/shared/shared/__tests__/ReactChildReconciler-test.js
1283-
* warns for duplicated keys
1284-
* warns for duplicated keys with component stack info
1283+
* warns for duplicated array keys
1284+
* warns for duplicated array keys with component stack info
1285+
* warns for duplicated iterable keys
1286+
* warns for duplicated iterable keys with component stack info
12851287

12861288
src/renderers/shared/shared/__tests__/ReactComponent-test.js
12871289
* should throw when supplying a ref outside of render method
@@ -1425,7 +1427,8 @@ src/renderers/shared/shared/__tests__/ReactMultiChild-test.js
14251427
* should replace children with different constructors
14261428
* should NOT replace children with different owners
14271429
* should replace children with different keys
1428-
* should warn for duplicated keys with component stack info
1430+
* should warn for duplicated array keys with component stack info
1431+
* should warn for duplicated iterable keys with component stack info
14291432
* should reorder bailed-out children
14301433

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

src/renderers/shared/fiber/ReactChildFiber.js

Lines changed: 66 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -550,39 +550,44 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
550550
function warnOnDuplicateKey(
551551
child : mixed,
552552
returnFiber : Fiber,
553-
knownKeys : Set<string>
554-
) {
553+
knownKeys : Set<string> | null
554+
) : Set<string> | null {
555555
if (__DEV__) {
556-
if (child == null || typeof child !== 'object') {
557-
return;
556+
if (typeof child !== 'object' || child == null) {
557+
return knownKeys;
558558
}
559559
switch (child.$$typeof) {
560-
// They have keys.
561560
case REACT_ELEMENT_TYPE:
562561
case REACT_COROUTINE_TYPE:
563562
case REACT_YIELD_TYPE:
564563
case REACT_PORTAL_TYPE:
565564
const key = child.key;
566565
if (typeof key !== 'string') {
567-
return;
566+
break;
568567
}
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 {
568+
if (knownKeys == null) {
569+
knownKeys = new Set();
570+
knownKeys.add(key);
571+
break;
572+
}
573+
if (!knownKeys.has(key)) {
579574
knownKeys.add(key);
575+
break;
580576
}
581-
return;
577+
warning(
578+
false,
579+
'Encountered two children with the same key, ' +
580+
'`%s`. Child keys must be unique; when two children share a key, ' +
581+
'only the first child will be used.%s',
582+
key,
583+
getStackAddendumByFiber(returnFiber)
584+
);
585+
break;
582586
default:
583-
return;
587+
break;
584588
}
585589
}
590+
return knownKeys;
586591
}
587592

588593
function reconcileChildrenArray(
@@ -610,11 +615,18 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
610615
// If you change this code, also update reconcileChildrenIterator() which
611616
// uses the same algorithm.
612617

618+
if (__DEV__) {
619+
// First, validate keys.
620+
let knownKeys = null;
621+
for (let i = 0; i < newChildren.length; i++) {
622+
const child = newChildren[i];
623+
knownKeys = warnOnDuplicateKey(child, returnFiber, knownKeys);
624+
}
625+
}
626+
613627
let resultingFirstChild : ?Fiber = null;
614628
let previousNewFiber : ?Fiber = null;
615629

616-
let knownKeysInDev = null;
617-
618630
let oldFiber = currentFirstChild;
619631
let lastPlacedIndex = 0;
620632
let newIdx = 0;
@@ -628,15 +640,10 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
628640
nextOldFiber = oldFiber.sibling;
629641
}
630642
}
631-
const newChild = newChildren[newIdx];
632-
if (__DEV__) {
633-
knownKeysInDev = knownKeysInDev || new Set();
634-
warnOnDuplicateKey(newChild, returnFiber, knownKeysInDev);
635-
}
636643
const newFiber = updateSlot(
637644
returnFiber,
638645
oldFiber,
639-
newChild,
646+
newChildren[newIdx],
640647
priority
641648
);
642649
if (!newFiber) {
@@ -647,18 +654,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
647654
if (!oldFiber) {
648655
oldFiber = nextOldFiber;
649656
}
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-
}
662657
break;
663658
}
664659
if (shouldTrackSideEffects) {
@@ -693,14 +688,9 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
693688
// If we don't have any more existing children we can choose a fast path
694689
// since the rest will all be insertions.
695690
for (; newIdx < newChildren.length; newIdx++) {
696-
const newChild = newChildren[newIdx];
697-
if (__DEV__) {
698-
knownKeysInDev = knownKeysInDev || new Set();
699-
warnOnDuplicateKey(newChild, returnFiber, knownKeysInDev);
700-
}
701691
const newFiber = createChild(
702692
returnFiber,
703-
newChild,
693+
newChildren[newIdx],
704694
priority
705695
);
706696
if (!newFiber) {
@@ -723,16 +713,11 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
723713

724714
// Keep scanning and use the map to restore deleted items as moves.
725715
for (; newIdx < newChildren.length; newIdx++) {
726-
const newChild = newChildren[newIdx];
727-
if (__DEV__) {
728-
knownKeysInDev = knownKeysInDev || new Set();
729-
warnOnDuplicateKey(newChild, returnFiber, knownKeysInDev);
730-
}
731716
const newFiber = updateFromMap(
732717
existingChildren,
733718
returnFiber,
734719
newIdx,
735-
newChild,
720+
newChildren[newIdx],
736721
priority
737722
);
738723
if (newFiber) {
@@ -769,17 +754,40 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
769754
function reconcileChildrenIterator(
770755
returnFiber : Fiber,
771756
currentFirstChild : ?Fiber,
772-
newChildren : Iterator<*>,
757+
newChildrenIterable : Iterable<*>,
773758
priority : PriorityLevel) : ?Fiber {
774759

775760
// This is the same implementation as reconcileChildrenArray(),
776761
// but using the iterator instead.
777762

763+
const iteratorFn = getIteratorFn(newChildrenIterable);
764+
if (typeof iteratorFn !== 'function') {
765+
throw new Error('An object is not an iterable.');
766+
}
767+
768+
if (__DEV__) {
769+
// First, validate keys.
770+
// We'll get a different iterator later for the main pass.
771+
const newChildren = iteratorFn.call(newChildrenIterable);
772+
if (newChildren == null) {
773+
throw new Error('An iterable object provided no iterator.');
774+
}
775+
let knownKeys = null;
776+
let step = newChildren.next();
777+
for (; !step.done; step = newChildren.next()) {
778+
const child = step.value;
779+
knownKeys = warnOnDuplicateKey(child, returnFiber, knownKeys);
780+
}
781+
}
782+
783+
const newChildren = iteratorFn.call(newChildrenIterable);
784+
if (newChildren == null) {
785+
throw new Error('An iterable object provided no iterator.');
786+
}
787+
778788
let resultingFirstChild : ?Fiber = null;
779789
let previousNewFiber : ?Fiber = null;
780790

781-
let knownKeysInDev = null;
782-
783791
let oldFiber = currentFirstChild;
784792
let lastPlacedIndex = 0;
785793
let newIdx = 0;
@@ -795,15 +803,10 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
795803
nextOldFiber = oldFiber.sibling;
796804
}
797805
}
798-
const newChild = step.value;
799-
if (__DEV__) {
800-
knownKeysInDev = knownKeysInDev || new Set();
801-
warnOnDuplicateKey(newChild, returnFiber, knownKeysInDev);
802-
}
803806
const newFiber = updateSlot(
804807
returnFiber,
805808
oldFiber,
806-
newChild,
809+
step.value,
807810
priority
808811
);
809812
if (!newFiber) {
@@ -814,18 +817,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
814817
if (!oldFiber) {
815818
oldFiber = nextOldFiber;
816819
}
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-
}
829820
break;
830821
}
831822
if (shouldTrackSideEffects) {
@@ -860,14 +851,9 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
860851
// If we don't have any more existing children we can choose a fast path
861852
// since the rest will all be insertions.
862853
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-
}
868854
const newFiber = createChild(
869855
returnFiber,
870-
newChild,
856+
step.value,
871857
priority
872858
);
873859
if (!newFiber) {
@@ -890,16 +876,11 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
890876

891877
// Keep scanning and use the map to restore deleted items as moves.
892878
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-
}
898879
const newFiber = updateFromMap(
899880
existingChildren,
900881
returnFiber,
901882
newIdx,
902-
newChild,
883+
step.value,
903884
priority
904885
);
905886
if (newFiber) {
@@ -1172,16 +1153,11 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
11721153
);
11731154
}
11741155

1175-
const iteratorFn = getIteratorFn(newChild);
1176-
if (iteratorFn) {
1177-
const iterator = iteratorFn.call(newChild);
1178-
if (iterator == null) {
1179-
throw new Error('An iterable object provided no iterator.');
1180-
}
1156+
if (getIteratorFn(newChild)) {
11811157
return reconcileChildrenIterator(
11821158
returnFiber,
11831159
currentFirstChild,
1184-
iterator,
1160+
newChild,
11851161
priority
11861162
);
11871163
}

0 commit comments

Comments
 (0)