Skip to content

Commit 73579d3

Browse files
sebmarkbagekoto
authored andcommitted
Reconcile element types of lazy component yielding the same type (facebook#20357)
* Reconcile element types of lazy component yielding the same type * Add some legacy mode and suspense boundary flushing tests * Fix infinite loop in legacy mode In legacy mode we typically commit the suspending fiber and then rerender the nearest boundary to render the fallback in a separate commit. We can't do that when the boundary itself suspends because when we try to do the second pass, it'll suspend again and infinite loop. Interestingly the legacy semantics are not needed in this case because they exist to let an existing partial render fully commit its partial state. In this case there's no partial state, so we can just render the fallback immediately instead. * Check fast refresh compatibility first resolveLazy can suspend and if it does, it can resuspend. Fast refresh assumes that we don't resuspend. Instead it relies on updating the inner component later. * Use timers instead of act to force fallbacks to show
1 parent e09c6b3 commit 73579d3

File tree

5 files changed

+407
-102
lines changed

5 files changed

+407
-102
lines changed

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

+64-50
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,11 +389,32 @@ 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 ||
389405
// Keep this check inline so it only runs on the false path:
390-
(__DEV__ ? isCompatibleFamilyForHotReloading(current, element) : false)
406+
(__DEV__
407+
? isCompatibleFamilyForHotReloading(current, element)
408+
: false) ||
409+
// Lazy types should reconcile their resolved type.
410+
// We need to do this after the Hot Reloading check above,
411+
// because hot reloading has different semantics than prod because
412+
// it doesn't resuspend. So we can't let the call below suspend.
413+
(enableLazyElements &&
414+
typeof elementType === 'object' &&
415+
elementType !== null &&
416+
elementType.$$typeof === REACT_LAZY_TYPE &&
417+
resolveLazy(elementType) === current.type)
391418
) {
392419
// Move based on index
393420
const existing = useFiber(current, element.props);
@@ -551,15 +578,6 @@ function ChildReconciler(shouldTrackSideEffects) {
551578
switch (newChild.$$typeof) {
552579
case REACT_ELEMENT_TYPE: {
553580
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-
}
563581
return updateElement(returnFiber, oldFiber, newChild, lanes);
564582
} else {
565583
return null;
@@ -622,15 +640,6 @@ function ChildReconciler(shouldTrackSideEffects) {
622640
existingChildren.get(
623641
newChild.key === null ? newIdx : newChild.key,
624642
) || 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-
}
634643
return updateElement(returnFiber, matchedFiber, newChild, lanes);
635644
}
636645
case REACT_PORTAL_TYPE: {
@@ -1101,39 +1110,44 @@ function ChildReconciler(shouldTrackSideEffects) {
11011110
// TODO: If key === null and child.key === null, then this only applies to
11021111
// the first item in the list.
11031112
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;
1113+
const elementType = element.type;
1114+
if (elementType === REACT_FRAGMENT_TYPE) {
1115+
if (child.tag === Fragment) {
1116+
deleteRemainingChildren(returnFiber, child.sibling);
1117+
const existing = useFiber(child, element.props.children);
1118+
existing.return = returnFiber;
1119+
if (__DEV__) {
1120+
existing._debugSource = element._source;
1121+
existing._debugOwner = element._owner;
11151122
}
1116-
break;
1123+
return existing;
11171124
}
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;
1125+
} else {
1126+
if (
1127+
child.elementType === elementType ||
1128+
// Keep this check inline so it only runs on the false path:
1129+
(__DEV__
1130+
? isCompatibleFamilyForHotReloading(child, element)
1131+
: false) ||
1132+
// Lazy types should reconcile their resolved type.
1133+
// We need to do this after the Hot Reloading check above,
1134+
// because hot reloading has different semantics than prod because
1135+
// it doesn't resuspend. So we can't let the call below suspend.
1136+
(enableLazyElements &&
1137+
typeof elementType === 'object' &&
1138+
elementType !== null &&
1139+
elementType.$$typeof === REACT_LAZY_TYPE &&
1140+
resolveLazy(elementType) === child.type)
1141+
) {
1142+
deleteRemainingChildren(returnFiber, child.sibling);
1143+
const existing = useFiber(child, element.props);
1144+
existing.ref = coerceRef(returnFiber, child, element);
1145+
existing.return = returnFiber;
1146+
if (__DEV__) {
1147+
existing._debugSource = element._source;
1148+
existing._debugOwner = element._owner;
11351149
}
1136-
break;
1150+
return existing;
11371151
}
11381152
}
11391153
// Didn't match.

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

+63-50
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,11 +389,32 @@ 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 ||
389405
// Keep this check inline so it only runs on the false path:
390-
(__DEV__ ? isCompatibleFamilyForHotReloading(current, element) : false)
406+
(__DEV__
407+
? isCompatibleFamilyForHotReloading(current, element)
408+
: false) ||
409+
// Lazy types should reconcile their resolved type.
410+
// We need to do this after the Hot Reloading check above,
411+
// because hot reloading has different semantics than prod because
412+
// it doesn't resuspend. So we can't let the call below suspend.
413+
(enableLazyElements &&
414+
typeof elementType === 'object' &&
415+
elementType !== null &&
416+
elementType.$$typeof === REACT_LAZY_TYPE &&
417+
resolveLazy(elementType) === current.type)
391418
) {
392419
// Move based on index
393420
const existing = useFiber(current, element.props);
@@ -551,15 +578,6 @@ function ChildReconciler(shouldTrackSideEffects) {
551578
switch (newChild.$$typeof) {
552579
case REACT_ELEMENT_TYPE: {
553580
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-
}
563581
return updateElement(returnFiber, oldFiber, newChild, lanes);
564582
} else {
565583
return null;
@@ -622,15 +640,6 @@ function ChildReconciler(shouldTrackSideEffects) {
622640
existingChildren.get(
623641
newChild.key === null ? newIdx : newChild.key,
624642
) || 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-
}
634643
return updateElement(returnFiber, matchedFiber, newChild, lanes);
635644
}
636645
case REACT_PORTAL_TYPE: {
@@ -1101,39 +1110,43 @@ function ChildReconciler(shouldTrackSideEffects) {
11011110
// TODO: If key === null and child.key === null, then this only applies to
11021111
// the first item in the list.
11031112
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;
1113+
const elementType = element.type;
1114+
if (elementType === REACT_FRAGMENT_TYPE) {
1115+
if (child.tag === Fragment) {
1116+
deleteRemainingChildren(returnFiber, child.sibling);
1117+
const existing = useFiber(child, element.props.children);
1118+
existing.return = returnFiber;
1119+
if (__DEV__) {
1120+
existing._debugSource = element._source;
1121+
existing._debugOwner = element._owner;
11151122
}
1116-
break;
1123+
return existing;
11171124
}
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;
1125+
} else {
1126+
if (
1127+
child.elementType === elementType ||
1128+
(__DEV__
1129+
? isCompatibleFamilyForHotReloading(child, element)
1130+
: false) ||
1131+
// Lazy types should reconcile their resolved type.
1132+
// We need to do this after the Hot Reloading check above,
1133+
// because hot reloading has different semantics than prod because
1134+
// it doesn't resuspend. So we can't let the call below suspend.
1135+
(enableLazyElements &&
1136+
typeof elementType === 'object' &&
1137+
elementType !== null &&
1138+
elementType.$$typeof === REACT_LAZY_TYPE &&
1139+
resolveLazy(elementType) === child.type)
1140+
) {
1141+
deleteRemainingChildren(returnFiber, child.sibling);
1142+
const existing = useFiber(child, element.props);
1143+
existing.ref = coerceRef(returnFiber, child, element);
1144+
existing.return = returnFiber;
1145+
if (__DEV__) {
1146+
existing._debugSource = element._source;
1147+
existing._debugOwner = element._owner;
11351148
}
1136-
break;
1149+
return existing;
11371150
}
11381151
}
11391152
// Didn't match.

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,15 @@ function throwException(
256256
// Note: It doesn't matter whether the component that suspended was
257257
// inside a blocking mode tree. If the Suspense is outside of it, we
258258
// should *not* suspend the commit.
259-
if ((workInProgress.mode & BlockingMode) === NoMode) {
259+
//
260+
// If the suspense boundary suspended itself suspended, we don't have to
261+
// do this trick because nothing was partially started. We can just
262+
// directly do a second pass over the fallback in this render and
263+
// pretend we meant to render that directly.
264+
if (
265+
(workInProgress.mode & BlockingMode) === NoMode &&
266+
workInProgress !== returnFiber
267+
) {
260268
workInProgress.flags |= DidCapture;
261269
sourceFiber.flags |= ForceUpdateForLegacySuspense;
262270

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,15 @@ function throwException(
256256
// Note: It doesn't matter whether the component that suspended was
257257
// inside a blocking mode tree. If the Suspense is outside of it, we
258258
// should *not* suspend the commit.
259-
if ((workInProgress.mode & BlockingMode) === NoMode) {
259+
//
260+
// If the suspense boundary suspended itself suspended, we don't have to
261+
// do this trick because nothing was partially started. We can just
262+
// directly do a second pass over the fallback in this render and
263+
// pretend we meant to render that directly.
264+
if (
265+
(workInProgress.mode & BlockingMode) === NoMode &&
266+
workInProgress !== returnFiber
267+
) {
260268
workInProgress.flags |= DidCapture;
261269
sourceFiber.flags |= ForceUpdateForLegacySuspense;
262270

0 commit comments

Comments
 (0)