diff --git a/packages/react-reconciler/src/ReactChildFiber.new.js b/packages/react-reconciler/src/ReactChildFiber.new.js
index 601f3b21e4395..534d7032ba2d9 100644
--- a/packages/react-reconciler/src/ReactChildFiber.new.js
+++ b/packages/react-reconciler/src/ReactChildFiber.new.js
@@ -246,6 +246,12 @@ function warnOnFunctionType(returnFiber: Fiber) {
}
}
+function resolveLazy(lazyType) {
+ const payload = lazyType._payload;
+ const init = lazyType._init;
+ return init(payload);
+}
+
// This wrapper function exists because I expect to clone the code in each path
// to be able to optimize each path individually by branching early. This needs
// a compiler or we can do it manually. Helpers that don't need this branching
@@ -383,11 +389,32 @@ function ChildReconciler(shouldTrackSideEffects) {
element: ReactElement,
lanes: Lanes,
): Fiber {
+ const elementType = element.type;
+ if (elementType === REACT_FRAGMENT_TYPE) {
+ return updateFragment(
+ returnFiber,
+ current,
+ element.props.children,
+ lanes,
+ element.key,
+ );
+ }
if (current !== null) {
if (
- current.elementType === element.type ||
+ current.elementType === elementType ||
// Keep this check inline so it only runs on the false path:
- (__DEV__ ? isCompatibleFamilyForHotReloading(current, element) : false)
+ (__DEV__
+ ? isCompatibleFamilyForHotReloading(current, element)
+ : false) ||
+ // Lazy types should reconcile their resolved type.
+ // We need to do this after the Hot Reloading check above,
+ // because hot reloading has different semantics than prod because
+ // it doesn't resuspend. So we can't let the call below suspend.
+ (enableLazyElements &&
+ typeof elementType === 'object' &&
+ elementType !== null &&
+ elementType.$$typeof === REACT_LAZY_TYPE &&
+ resolveLazy(elementType) === current.type)
) {
// Move based on index
const existing = useFiber(current, element.props);
@@ -551,15 +578,6 @@ function ChildReconciler(shouldTrackSideEffects) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === key) {
- if (newChild.type === REACT_FRAGMENT_TYPE) {
- return updateFragment(
- returnFiber,
- oldFiber,
- newChild.props.children,
- lanes,
- key,
- );
- }
return updateElement(returnFiber, oldFiber, newChild, lanes);
} else {
return null;
@@ -622,15 +640,6 @@ function ChildReconciler(shouldTrackSideEffects) {
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
) || null;
- if (newChild.type === REACT_FRAGMENT_TYPE) {
- return updateFragment(
- returnFiber,
- matchedFiber,
- newChild.props.children,
- lanes,
- newChild.key,
- );
- }
return updateElement(returnFiber, matchedFiber, newChild, lanes);
}
case REACT_PORTAL_TYPE: {
@@ -1101,39 +1110,44 @@ function ChildReconciler(shouldTrackSideEffects) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
- switch (child.tag) {
- case Fragment: {
- if (element.type === REACT_FRAGMENT_TYPE) {
- deleteRemainingChildren(returnFiber, child.sibling);
- const existing = useFiber(child, element.props.children);
- existing.return = returnFiber;
- if (__DEV__) {
- existing._debugSource = element._source;
- existing._debugOwner = element._owner;
- }
- return existing;
+ const elementType = element.type;
+ if (elementType === REACT_FRAGMENT_TYPE) {
+ if (child.tag === Fragment) {
+ deleteRemainingChildren(returnFiber, child.sibling);
+ const existing = useFiber(child, element.props.children);
+ existing.return = returnFiber;
+ if (__DEV__) {
+ existing._debugSource = element._source;
+ existing._debugOwner = element._owner;
}
- break;
+ return existing;
}
- default: {
- if (
- child.elementType === element.type ||
- // Keep this check inline so it only runs on the false path:
- (__DEV__
- ? isCompatibleFamilyForHotReloading(child, element)
- : false)
- ) {
- deleteRemainingChildren(returnFiber, child.sibling);
- const existing = useFiber(child, element.props);
- existing.ref = coerceRef(returnFiber, child, element);
- existing.return = returnFiber;
- if (__DEV__) {
- existing._debugSource = element._source;
- existing._debugOwner = element._owner;
- }
- return existing;
+ } else {
+ if (
+ child.elementType === elementType ||
+ // Keep this check inline so it only runs on the false path:
+ (__DEV__
+ ? isCompatibleFamilyForHotReloading(child, element)
+ : false) ||
+ // Lazy types should reconcile their resolved type.
+ // We need to do this after the Hot Reloading check above,
+ // because hot reloading has different semantics than prod because
+ // it doesn't resuspend. So we can't let the call below suspend.
+ (enableLazyElements &&
+ typeof elementType === 'object' &&
+ elementType !== null &&
+ elementType.$$typeof === REACT_LAZY_TYPE &&
+ resolveLazy(elementType) === child.type)
+ ) {
+ deleteRemainingChildren(returnFiber, child.sibling);
+ const existing = useFiber(child, element.props);
+ existing.ref = coerceRef(returnFiber, child, element);
+ existing.return = returnFiber;
+ if (__DEV__) {
+ existing._debugSource = element._source;
+ existing._debugOwner = element._owner;
}
- break;
+ return existing;
}
}
// Didn't match.
diff --git a/packages/react-reconciler/src/ReactChildFiber.old.js b/packages/react-reconciler/src/ReactChildFiber.old.js
index adb9c0418df0e..df4c2e10d9299 100644
--- a/packages/react-reconciler/src/ReactChildFiber.old.js
+++ b/packages/react-reconciler/src/ReactChildFiber.old.js
@@ -246,6 +246,12 @@ function warnOnFunctionType(returnFiber: Fiber) {
}
}
+function resolveLazy(lazyType) {
+ const payload = lazyType._payload;
+ const init = lazyType._init;
+ return init(payload);
+}
+
// This wrapper function exists because I expect to clone the code in each path
// to be able to optimize each path individually by branching early. This needs
// a compiler or we can do it manually. Helpers that don't need this branching
@@ -383,11 +389,32 @@ function ChildReconciler(shouldTrackSideEffects) {
element: ReactElement,
lanes: Lanes,
): Fiber {
+ const elementType = element.type;
+ if (elementType === REACT_FRAGMENT_TYPE) {
+ return updateFragment(
+ returnFiber,
+ current,
+ element.props.children,
+ lanes,
+ element.key,
+ );
+ }
if (current !== null) {
if (
- current.elementType === element.type ||
+ current.elementType === elementType ||
// Keep this check inline so it only runs on the false path:
- (__DEV__ ? isCompatibleFamilyForHotReloading(current, element) : false)
+ (__DEV__
+ ? isCompatibleFamilyForHotReloading(current, element)
+ : false) ||
+ // Lazy types should reconcile their resolved type.
+ // We need to do this after the Hot Reloading check above,
+ // because hot reloading has different semantics than prod because
+ // it doesn't resuspend. So we can't let the call below suspend.
+ (enableLazyElements &&
+ typeof elementType === 'object' &&
+ elementType !== null &&
+ elementType.$$typeof === REACT_LAZY_TYPE &&
+ resolveLazy(elementType) === current.type)
) {
// Move based on index
const existing = useFiber(current, element.props);
@@ -551,15 +578,6 @@ function ChildReconciler(shouldTrackSideEffects) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === key) {
- if (newChild.type === REACT_FRAGMENT_TYPE) {
- return updateFragment(
- returnFiber,
- oldFiber,
- newChild.props.children,
- lanes,
- key,
- );
- }
return updateElement(returnFiber, oldFiber, newChild, lanes);
} else {
return null;
@@ -622,15 +640,6 @@ function ChildReconciler(shouldTrackSideEffects) {
existingChildren.get(
newChild.key === null ? newIdx : newChild.key,
) || null;
- if (newChild.type === REACT_FRAGMENT_TYPE) {
- return updateFragment(
- returnFiber,
- matchedFiber,
- newChild.props.children,
- lanes,
- newChild.key,
- );
- }
return updateElement(returnFiber, matchedFiber, newChild, lanes);
}
case REACT_PORTAL_TYPE: {
@@ -1101,39 +1110,43 @@ function ChildReconciler(shouldTrackSideEffects) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
- switch (child.tag) {
- case Fragment: {
- if (element.type === REACT_FRAGMENT_TYPE) {
- deleteRemainingChildren(returnFiber, child.sibling);
- const existing = useFiber(child, element.props.children);
- existing.return = returnFiber;
- if (__DEV__) {
- existing._debugSource = element._source;
- existing._debugOwner = element._owner;
- }
- return existing;
+ const elementType = element.type;
+ if (elementType === REACT_FRAGMENT_TYPE) {
+ if (child.tag === Fragment) {
+ deleteRemainingChildren(returnFiber, child.sibling);
+ const existing = useFiber(child, element.props.children);
+ existing.return = returnFiber;
+ if (__DEV__) {
+ existing._debugSource = element._source;
+ existing._debugOwner = element._owner;
}
- break;
+ return existing;
}
- default: {
- if (
- child.elementType === element.type ||
- // Keep this check inline so it only runs on the false path:
- (__DEV__
- ? isCompatibleFamilyForHotReloading(child, element)
- : false)
- ) {
- deleteRemainingChildren(returnFiber, child.sibling);
- const existing = useFiber(child, element.props);
- existing.ref = coerceRef(returnFiber, child, element);
- existing.return = returnFiber;
- if (__DEV__) {
- existing._debugSource = element._source;
- existing._debugOwner = element._owner;
- }
- return existing;
+ } else {
+ if (
+ child.elementType === elementType ||
+ (__DEV__
+ ? isCompatibleFamilyForHotReloading(child, element)
+ : false) ||
+ // Lazy types should reconcile their resolved type.
+ // We need to do this after the Hot Reloading check above,
+ // because hot reloading has different semantics than prod because
+ // it doesn't resuspend. So we can't let the call below suspend.
+ (enableLazyElements &&
+ typeof elementType === 'object' &&
+ elementType !== null &&
+ elementType.$$typeof === REACT_LAZY_TYPE &&
+ resolveLazy(elementType) === child.type)
+ ) {
+ deleteRemainingChildren(returnFiber, child.sibling);
+ const existing = useFiber(child, element.props);
+ existing.ref = coerceRef(returnFiber, child, element);
+ existing.return = returnFiber;
+ if (__DEV__) {
+ existing._debugSource = element._source;
+ existing._debugOwner = element._owner;
}
- break;
+ return existing;
}
}
// Didn't match.
diff --git a/packages/react-reconciler/src/ReactFiberThrow.new.js b/packages/react-reconciler/src/ReactFiberThrow.new.js
index 9dd9df6d0cfe5..01ab69cecee66 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.new.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.new.js
@@ -256,7 +256,15 @@ function throwException(
// Note: It doesn't matter whether the component that suspended was
// inside a blocking mode tree. If the Suspense is outside of it, we
// should *not* suspend the commit.
- if ((workInProgress.mode & BlockingMode) === NoMode) {
+ //
+ // If the suspense boundary suspended itself suspended, we don't have to
+ // do this trick because nothing was partially started. We can just
+ // directly do a second pass over the fallback in this render and
+ // pretend we meant to render that directly.
+ if (
+ (workInProgress.mode & BlockingMode) === NoMode &&
+ workInProgress !== returnFiber
+ ) {
workInProgress.flags |= DidCapture;
sourceFiber.flags |= ForceUpdateForLegacySuspense;
diff --git a/packages/react-reconciler/src/ReactFiberThrow.old.js b/packages/react-reconciler/src/ReactFiberThrow.old.js
index 8d338d552d7d6..fabcffa87378a 100644
--- a/packages/react-reconciler/src/ReactFiberThrow.old.js
+++ b/packages/react-reconciler/src/ReactFiberThrow.old.js
@@ -256,7 +256,15 @@ function throwException(
// Note: It doesn't matter whether the component that suspended was
// inside a blocking mode tree. If the Suspense is outside of it, we
// should *not* suspend the commit.
- if ((workInProgress.mode & BlockingMode) === NoMode) {
+ //
+ // If the suspense boundary suspended itself suspended, we don't have to
+ // do this trick because nothing was partially started. We can just
+ // directly do a second pass over the fallback in this render and
+ // pretend we meant to render that directly.
+ if (
+ (workInProgress.mode & BlockingMode) === NoMode &&
+ workInProgress !== returnFiber
+ ) {
workInProgress.flags |= DidCapture;
sourceFiber.flags |= ForceUpdateForLegacySuspense;
diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
index 55c15e7412eab..b7f1570823c14 100644
--- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
@@ -1268,6 +1268,192 @@ describe('ReactLazy', () => {
expect(componentStackMessage).toContain('in Lazy');
});
+ // @gate enableLazyElements
+ it('mount and reorder lazy types', async () => {
+ class Child extends React.Component {
+ componentDidMount() {
+ Scheduler.unstable_yieldValue('Did mount: ' + this.props.label);
+ }
+ componentDidUpdate() {
+ Scheduler.unstable_yieldValue('Did update: ' + this.props.label);
+ }
+ render() {
+ return ;
+ }
+ }
+
+ function ChildA({lowerCase}) {
+ return ;
+ }
+
+ function ChildB({lowerCase}) {
+ return ;
+ }
+
+ const LazyChildA = lazy(() => {
+ Scheduler.unstable_yieldValue('Init A');
+ return fakeImport(ChildA);
+ });
+ const LazyChildB = lazy(() => {
+ Scheduler.unstable_yieldValue('Init B');
+ return fakeImport(ChildB);
+ });
+ const LazyChildA2 = lazy(() => {
+ Scheduler.unstable_yieldValue('Init A2');
+ return fakeImport(ChildA);
+ });
+ let resolveB2;
+ const LazyChildB2 = lazy(() => {
+ Scheduler.unstable_yieldValue('Init B2');
+ return new Promise(r => {
+ resolveB2 = r;
+ });
+ });
+
+ function Parent({swap}) {
+ return (
+ }>
+ }>
+ {swap
+ ? [
+ ,
+ ,
+ ]
+ : [, ]}
+
+
+ );
+ }
+
+ const root = ReactTestRenderer.create(, {
+ unstable_isConcurrent: true,
+ });
+
+ expect(Scheduler).toFlushAndYield(['Init A', 'Init B', 'Loading...']);
+ expect(root).not.toMatchRenderedOutput('AB');
+
+ await LazyChildA;
+ await LazyChildB;
+
+ expect(Scheduler).toFlushAndYield([
+ 'A',
+ 'B',
+ 'Did mount: A',
+ 'Did mount: B',
+ ]);
+ expect(root).toMatchRenderedOutput('AB');
+
+ // Swap the position of A and B
+ root.update();
+ expect(Scheduler).toFlushAndYield(['Init B2', 'Loading...']);
+ jest.runAllTimers();
+
+ // The suspense boundary should've triggered now.
+ expect(root).toMatchRenderedOutput('Loading...');
+ await resolveB2({default: ChildB});
+
+ // We need to flush to trigger the second one to load.
+ expect(Scheduler).toFlushAndYield(['Init A2']);
+ await LazyChildA2;
+
+ expect(Scheduler).toFlushAndYield([
+ 'b',
+ 'a',
+ 'Did update: b',
+ 'Did update: a',
+ ]);
+ expect(root).toMatchRenderedOutput('ba');
+ });
+
+ // @gate enableLazyElements
+ it('mount and reorder lazy types (legacy mode)', async () => {
+ class Child extends React.Component {
+ componentDidMount() {
+ Scheduler.unstable_yieldValue('Did mount: ' + this.props.label);
+ }
+ componentDidUpdate() {
+ Scheduler.unstable_yieldValue('Did update: ' + this.props.label);
+ }
+ render() {
+ return ;
+ }
+ }
+
+ function ChildA({lowerCase}) {
+ return ;
+ }
+
+ function ChildB({lowerCase}) {
+ return ;
+ }
+
+ const LazyChildA = lazy(() => {
+ Scheduler.unstable_yieldValue('Init A');
+ return fakeImport(ChildA);
+ });
+ const LazyChildB = lazy(() => {
+ Scheduler.unstable_yieldValue('Init B');
+ return fakeImport(ChildB);
+ });
+ const LazyChildA2 = lazy(() => {
+ Scheduler.unstable_yieldValue('Init A2');
+ return fakeImport(ChildA);
+ });
+ const LazyChildB2 = lazy(() => {
+ Scheduler.unstable_yieldValue('Init B2');
+ return fakeImport(ChildB);
+ });
+
+ function Parent({swap}) {
+ return (
+ }>
+ }>
+ {swap
+ ? [
+ ,
+ ,
+ ]
+ : [, ]}
+
+
+ );
+ }
+
+ const root = ReactTestRenderer.create(, {
+ unstable_isConcurrent: false,
+ });
+
+ expect(Scheduler).toHaveYielded(['Init A', 'Init B', 'Loading...']);
+ expect(root).not.toMatchRenderedOutput('AB');
+
+ await LazyChildA;
+ await LazyChildB;
+
+ expect(Scheduler).toFlushAndYield([
+ 'A',
+ 'B',
+ 'Did mount: A',
+ 'Did mount: B',
+ ]);
+ expect(root).toMatchRenderedOutput('AB');
+
+ // Swap the position of A and B
+ root.update();
+ expect(Scheduler).toHaveYielded(['Init B2', 'Loading...']);
+ await LazyChildB2;
+ // We need to flush to trigger the second one to load.
+ expect(Scheduler).toFlushAndYield(['Init A2']);
+ await LazyChildA2;
+
+ expect(Scheduler).toFlushAndYield([
+ 'b',
+ 'a',
+ 'Did update: b',
+ 'Did update: a',
+ ]);
+ expect(root).toMatchRenderedOutput('ba');
+ });
+
// @gate enableLazyElements
it('mount and reorder lazy elements', async () => {
class Child extends React.Component {
@@ -1343,4 +1529,80 @@ describe('ReactLazy', () => {
]);
expect(root).toMatchRenderedOutput('ba');
});
+
+ // @gate enableLazyElements
+ it('mount and reorder lazy elements (legacy mode)', async () => {
+ class Child extends React.Component {
+ componentDidMount() {
+ Scheduler.unstable_yieldValue('Did mount: ' + this.props.label);
+ }
+ componentDidUpdate() {
+ Scheduler.unstable_yieldValue('Did update: ' + this.props.label);
+ }
+ render() {
+ return ;
+ }
+ }
+
+ const lazyChildA = lazy(() => {
+ Scheduler.unstable_yieldValue('Init A');
+ return fakeImport();
+ });
+ const lazyChildB = lazy(() => {
+ Scheduler.unstable_yieldValue('Init B');
+ return fakeImport();
+ });
+ const lazyChildA2 = lazy(() => {
+ Scheduler.unstable_yieldValue('Init A2');
+ return fakeImport();
+ });
+ const lazyChildB2 = lazy(() => {
+ Scheduler.unstable_yieldValue('Init B2');
+ return fakeImport();
+ });
+
+ function Parent({swap}) {
+ return (
+ }>
+ {swap ? [lazyChildB2, lazyChildA2] : [lazyChildA, lazyChildB]}
+
+ );
+ }
+
+ const root = ReactTestRenderer.create(, {
+ unstable_isConcurrent: false,
+ });
+
+ expect(Scheduler).toHaveYielded(['Init A', 'Loading...']);
+ expect(root).not.toMatchRenderedOutput('AB');
+
+ await lazyChildA;
+ // We need to flush to trigger the B to load.
+ expect(Scheduler).toFlushAndYield(['Init B']);
+ await lazyChildB;
+
+ expect(Scheduler).toFlushAndYield([
+ 'A',
+ 'B',
+ 'Did mount: A',
+ 'Did mount: B',
+ ]);
+ expect(root).toMatchRenderedOutput('AB');
+
+ // Swap the position of A and B
+ root.update();
+ expect(Scheduler).toHaveYielded(['Init B2', 'Loading...']);
+ await lazyChildB2;
+ // We need to flush to trigger the second one to load.
+ expect(Scheduler).toFlushAndYield(['Init A2']);
+ await lazyChildA2;
+
+ expect(Scheduler).toFlushAndYield([
+ 'b',
+ 'a',
+ 'Did update: b',
+ 'Did update: a',
+ ]);
+ expect(root).toMatchRenderedOutput('ba');
+ });
});