Skip to content

Commit ffe59a8

Browse files
committed
Reconcile element types of lazy component yielding the same type
1 parent 3f73dce commit ffe59a8

File tree

3 files changed

+193
-98
lines changed

3 files changed

+193
-98
lines changed

packages/react-reconciler/src/ReactChildFiber.new.js

+53-49
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,12 @@ function warnOnFunctionType(returnFiber: Fiber) {
246246
}
247247
}
248248

249+
function resolveLazy(lazyType) {
250+
const payload = lazyType._payload;
251+
const init = lazyType._init;
252+
return init(payload);
253+
}
254+
249255
// This wrapper function exists because I expect to clone the code in each path
250256
// to be able to optimize each path individually by branching early. This needs
251257
// a compiler or we can do it manually. Helpers that don't need this branching
@@ -383,9 +389,24 @@ function ChildReconciler(shouldTrackSideEffects) {
383389
element: ReactElement,
384390
lanes: Lanes,
385391
): Fiber {
392+
const elementType = element.type;
393+
if (elementType === REACT_FRAGMENT_TYPE) {
394+
return updateFragment(
395+
returnFiber,
396+
current,
397+
element.props.children,
398+
lanes,
399+
element.key,
400+
);
401+
}
386402
if (current !== null) {
387403
if (
388-
current.elementType === element.type ||
404+
current.elementType === elementType ||
405+
(enableLazyElements &&
406+
typeof elementType === 'object' &&
407+
elementType !== null &&
408+
elementType.$$typeof === REACT_LAZY_TYPE &&
409+
resolveLazy(elementType) === current.type) ||
389410
// Keep this check inline so it only runs on the false path:
390411
(__DEV__ ? isCompatibleFamilyForHotReloading(current, element) : false)
391412
) {
@@ -551,15 +572,6 @@ function ChildReconciler(shouldTrackSideEffects) {
551572
switch (newChild.$$typeof) {
552573
case REACT_ELEMENT_TYPE: {
553574
if (newChild.key === key) {
554-
if (newChild.type === REACT_FRAGMENT_TYPE) {
555-
return updateFragment(
556-
returnFiber,
557-
oldFiber,
558-
newChild.props.children,
559-
lanes,
560-
key,
561-
);
562-
}
563575
return updateElement(returnFiber, oldFiber, newChild, lanes);
564576
} else {
565577
return null;
@@ -622,15 +634,6 @@ function ChildReconciler(shouldTrackSideEffects) {
622634
existingChildren.get(
623635
newChild.key === null ? newIdx : newChild.key,
624636
) || null;
625-
if (newChild.type === REACT_FRAGMENT_TYPE) {
626-
return updateFragment(
627-
returnFiber,
628-
matchedFiber,
629-
newChild.props.children,
630-
lanes,
631-
newChild.key,
632-
);
633-
}
634637
return updateElement(returnFiber, matchedFiber, newChild, lanes);
635638
}
636639
case REACT_PORTAL_TYPE: {
@@ -1101,39 +1104,40 @@ function ChildReconciler(shouldTrackSideEffects) {
11011104
// TODO: If key === null and child.key === null, then this only applies to
11021105
// the first item in the list.
11031106
if (child.key === key) {
1104-
switch (child.tag) {
1105-
case Fragment: {
1106-
if (element.type === REACT_FRAGMENT_TYPE) {
1107-
deleteRemainingChildren(returnFiber, child.sibling);
1108-
const existing = useFiber(child, element.props.children);
1109-
existing.return = returnFiber;
1110-
if (__DEV__) {
1111-
existing._debugSource = element._source;
1112-
existing._debugOwner = element._owner;
1113-
}
1114-
return existing;
1107+
const elementType = element.type;
1108+
if (elementType === REACT_FRAGMENT_TYPE) {
1109+
if (child.tag === Fragment) {
1110+
deleteRemainingChildren(returnFiber, child.sibling);
1111+
const existing = useFiber(child, element.props.children);
1112+
existing.return = returnFiber;
1113+
if (__DEV__) {
1114+
existing._debugSource = element._source;
1115+
existing._debugOwner = element._owner;
11151116
}
1116-
break;
1117+
return existing;
11171118
}
1118-
default: {
1119-
if (
1120-
child.elementType === element.type ||
1121-
// Keep this check inline so it only runs on the false path:
1122-
(__DEV__
1123-
? isCompatibleFamilyForHotReloading(child, element)
1124-
: false)
1125-
) {
1126-
deleteRemainingChildren(returnFiber, child.sibling);
1127-
const existing = useFiber(child, element.props);
1128-
existing.ref = coerceRef(returnFiber, child, element);
1129-
existing.return = returnFiber;
1130-
if (__DEV__) {
1131-
existing._debugSource = element._source;
1132-
existing._debugOwner = element._owner;
1133-
}
1134-
return existing;
1119+
} else {
1120+
if (
1121+
child.elementType === elementType ||
1122+
(enableLazyElements &&
1123+
typeof elementType === 'object' &&
1124+
elementType !== null &&
1125+
elementType.$$typeof === REACT_LAZY_TYPE &&
1126+
resolveLazy(elementType) === child.type) ||
1127+
// Keep this check inline so it only runs on the false path:
1128+
(__DEV__
1129+
? isCompatibleFamilyForHotReloading(child, element)
1130+
: false)
1131+
) {
1132+
deleteRemainingChildren(returnFiber, child.sibling);
1133+
const existing = useFiber(child, element.props);
1134+
existing.ref = coerceRef(returnFiber, child, element);
1135+
existing.return = returnFiber;
1136+
if (__DEV__) {
1137+
existing._debugSource = element._source;
1138+
existing._debugOwner = element._owner;
11351139
}
1136-
break;
1140+
return existing;
11371141
}
11381142
}
11391143
// Didn't match.

packages/react-reconciler/src/ReactChildFiber.old.js

+53-49
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,12 @@ function warnOnFunctionType(returnFiber: Fiber) {
246246
}
247247
}
248248

249+
function resolveLazy(lazyType) {
250+
const payload = lazyType._payload;
251+
const init = lazyType._init;
252+
return init(payload);
253+
}
254+
249255
// This wrapper function exists because I expect to clone the code in each path
250256
// to be able to optimize each path individually by branching early. This needs
251257
// a compiler or we can do it manually. Helpers that don't need this branching
@@ -383,9 +389,24 @@ function ChildReconciler(shouldTrackSideEffects) {
383389
element: ReactElement,
384390
lanes: Lanes,
385391
): Fiber {
392+
const elementType = element.type;
393+
if (elementType === REACT_FRAGMENT_TYPE) {
394+
return updateFragment(
395+
returnFiber,
396+
current,
397+
element.props.children,
398+
lanes,
399+
element.key,
400+
);
401+
}
386402
if (current !== null) {
387403
if (
388-
current.elementType === element.type ||
404+
current.elementType === elementType ||
405+
(enableLazyElements &&
406+
typeof elementType === 'object' &&
407+
elementType !== null &&
408+
elementType.$$typeof === REACT_LAZY_TYPE &&
409+
resolveLazy(elementType) === current.type) ||
389410
// Keep this check inline so it only runs on the false path:
390411
(__DEV__ ? isCompatibleFamilyForHotReloading(current, element) : false)
391412
) {
@@ -551,15 +572,6 @@ function ChildReconciler(shouldTrackSideEffects) {
551572
switch (newChild.$$typeof) {
552573
case REACT_ELEMENT_TYPE: {
553574
if (newChild.key === key) {
554-
if (newChild.type === REACT_FRAGMENT_TYPE) {
555-
return updateFragment(
556-
returnFiber,
557-
oldFiber,
558-
newChild.props.children,
559-
lanes,
560-
key,
561-
);
562-
}
563575
return updateElement(returnFiber, oldFiber, newChild, lanes);
564576
} else {
565577
return null;
@@ -622,15 +634,6 @@ function ChildReconciler(shouldTrackSideEffects) {
622634
existingChildren.get(
623635
newChild.key === null ? newIdx : newChild.key,
624636
) || null;
625-
if (newChild.type === REACT_FRAGMENT_TYPE) {
626-
return updateFragment(
627-
returnFiber,
628-
matchedFiber,
629-
newChild.props.children,
630-
lanes,
631-
newChild.key,
632-
);
633-
}
634637
return updateElement(returnFiber, matchedFiber, newChild, lanes);
635638
}
636639
case REACT_PORTAL_TYPE: {
@@ -1101,39 +1104,40 @@ function ChildReconciler(shouldTrackSideEffects) {
11011104
// TODO: If key === null and child.key === null, then this only applies to
11021105
// the first item in the list.
11031106
if (child.key === key) {
1104-
switch (child.tag) {
1105-
case Fragment: {
1106-
if (element.type === REACT_FRAGMENT_TYPE) {
1107-
deleteRemainingChildren(returnFiber, child.sibling);
1108-
const existing = useFiber(child, element.props.children);
1109-
existing.return = returnFiber;
1110-
if (__DEV__) {
1111-
existing._debugSource = element._source;
1112-
existing._debugOwner = element._owner;
1113-
}
1114-
return existing;
1107+
const elementType = element.type;
1108+
if (elementType === REACT_FRAGMENT_TYPE) {
1109+
if (child.tag === Fragment) {
1110+
deleteRemainingChildren(returnFiber, child.sibling);
1111+
const existing = useFiber(child, element.props.children);
1112+
existing.return = returnFiber;
1113+
if (__DEV__) {
1114+
existing._debugSource = element._source;
1115+
existing._debugOwner = element._owner;
11151116
}
1116-
break;
1117+
return existing;
11171118
}
1118-
default: {
1119-
if (
1120-
child.elementType === element.type ||
1121-
// Keep this check inline so it only runs on the false path:
1122-
(__DEV__
1123-
? isCompatibleFamilyForHotReloading(child, element)
1124-
: false)
1125-
) {
1126-
deleteRemainingChildren(returnFiber, child.sibling);
1127-
const existing = useFiber(child, element.props);
1128-
existing.ref = coerceRef(returnFiber, child, element);
1129-
existing.return = returnFiber;
1130-
if (__DEV__) {
1131-
existing._debugSource = element._source;
1132-
existing._debugOwner = element._owner;
1133-
}
1134-
return existing;
1119+
} else {
1120+
if (
1121+
child.elementType === elementType ||
1122+
(enableLazyElements &&
1123+
typeof elementType === 'object' &&
1124+
elementType !== null &&
1125+
elementType.$$typeof === REACT_LAZY_TYPE &&
1126+
resolveLazy(elementType) === child.type) ||
1127+
// Keep this check inline so it only runs on the false path:
1128+
(__DEV__
1129+
? isCompatibleFamilyForHotReloading(child, element)
1130+
: false)
1131+
) {
1132+
deleteRemainingChildren(returnFiber, child.sibling);
1133+
const existing = useFiber(child, element.props);
1134+
existing.ref = coerceRef(returnFiber, child, element);
1135+
existing.return = returnFiber;
1136+
if (__DEV__) {
1137+
existing._debugSource = element._source;
1138+
existing._debugOwner = element._owner;
11351139
}
1136-
break;
1140+
return existing;
11371141
}
11381142
}
11391143
// Didn't match.

packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js

+87
Original file line numberDiff line numberDiff line change
@@ -1268,6 +1268,93 @@ describe('ReactLazy', () => {
12681268
expect(componentStackMessage).toContain('in Lazy');
12691269
});
12701270

1271+
// @gate enableLazyElements
1272+
it('mount and reorder lazy types', async () => {
1273+
class Child extends React.Component {
1274+
componentDidMount() {
1275+
Scheduler.unstable_yieldValue('Did mount: ' + this.props.label);
1276+
}
1277+
componentDidUpdate() {
1278+
Scheduler.unstable_yieldValue('Did update: ' + this.props.label);
1279+
}
1280+
render() {
1281+
return <Text text={this.props.label} />;
1282+
}
1283+
}
1284+
1285+
function ChildA({lowerCase}) {
1286+
return <Child label={lowerCase ? 'a' : 'A'} />;
1287+
}
1288+
1289+
function ChildB({lowerCase}) {
1290+
return <Child label={lowerCase ? 'b' : 'B'} />;
1291+
}
1292+
1293+
const LazyChildA = lazy(() => {
1294+
Scheduler.unstable_yieldValue('Init A');
1295+
return fakeImport(ChildA);
1296+
});
1297+
const LazyChildB = lazy(() => {
1298+
Scheduler.unstable_yieldValue('Init B');
1299+
return fakeImport(ChildB);
1300+
});
1301+
const LazyChildA2 = lazy(() => {
1302+
Scheduler.unstable_yieldValue('Init A2');
1303+
return fakeImport(ChildA);
1304+
});
1305+
const LazyChildB2 = lazy(() => {
1306+
Scheduler.unstable_yieldValue('Init B2');
1307+
return fakeImport(ChildB);
1308+
});
1309+
1310+
function Parent({swap}) {
1311+
return (
1312+
<Suspense fallback={<Text text="Loading..." />}>
1313+
{swap
1314+
? [
1315+
<LazyChildB2 key="B" lowerCase={true} />,
1316+
<LazyChildA2 key="A" lowerCase={true} />,
1317+
]
1318+
: [<LazyChildA key="A" />, <LazyChildB key="B" />]}
1319+
</Suspense>
1320+
);
1321+
}
1322+
1323+
const root = ReactTestRenderer.create(<Parent swap={false} />, {
1324+
unstable_isConcurrent: true,
1325+
});
1326+
1327+
expect(Scheduler).toFlushAndYield(['Init A', 'Init B', 'Loading...']);
1328+
expect(root).not.toMatchRenderedOutput('AB');
1329+
1330+
await LazyChildA;
1331+
await LazyChildB;
1332+
1333+
expect(Scheduler).toFlushAndYield([
1334+
'A',
1335+
'B',
1336+
'Did mount: A',
1337+
'Did mount: B',
1338+
]);
1339+
expect(root).toMatchRenderedOutput('AB');
1340+
1341+
// Swap the position of A and B
1342+
root.update(<Parent swap={true} />);
1343+
expect(Scheduler).toFlushAndYield(['Init B2', 'Loading...']);
1344+
await LazyChildB2;
1345+
// We need to flush to trigger the second one to load.
1346+
expect(Scheduler).toFlushAndYield(['Init A2', 'Loading...']);
1347+
await LazyChildA2;
1348+
1349+
expect(Scheduler).toFlushAndYield([
1350+
'b',
1351+
'a',
1352+
'Did update: b',
1353+
'Did update: a',
1354+
]);
1355+
expect(root).toMatchRenderedOutput('ba');
1356+
});
1357+
12711358
// @gate enableLazyElements
12721359
it('mount and reorder lazy elements', async () => {
12731360
class Child extends React.Component {

0 commit comments

Comments
 (0)