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'); + }); });