diff --git a/packages/react-reconciler/src/ReactChildFiber.new.js b/packages/react-reconciler/src/ReactChildFiber.new.js index 601f3b21e4395..1e8d9a7fa18cd 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,9 +389,24 @@ 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 || + (enableLazyElements && + typeof elementType === 'object' && + elementType !== null && + elementType.$$typeof === REACT_LAZY_TYPE && + resolveLazy(elementType) === current.type) || // Keep this check inline so it only runs on the false path: (__DEV__ ? isCompatibleFamilyForHotReloading(current, element) : false) ) { @@ -551,15 +572,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 +634,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 +1104,40 @@ 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; + let 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 || + (enableLazyElements && + typeof elementType === 'object' && + elementType !== null && + elementType.$$typeof === REACT_LAZY_TYPE && + resolveLazy(elementType) === child.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; } - 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..cde52aef0f6ab 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,9 +389,24 @@ 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 || + (enableLazyElements && + typeof elementType === 'object' && + elementType !== null && + elementType.$$typeof === REACT_LAZY_TYPE && + resolveLazy(elementType) === current.type) || // Keep this check inline so it only runs on the false path: (__DEV__ ? isCompatibleFamilyForHotReloading(current, element) : false) ) { @@ -551,15 +572,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 +634,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 +1104,40 @@ 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 || + (enableLazyElements && + typeof elementType === 'object' && + elementType !== null && + elementType.$$typeof === REACT_LAZY_TYPE && + resolveLazy(elementType) === child.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; } - break; + return existing; } } // Didn't match. diff --git a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js index 55c15e7412eab..80ab9b132de27 100644 --- a/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js @@ -1268,6 +1268,93 @@ 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); + }); + const LazyChildB2 = lazy(() => { + Scheduler.unstable_yieldValue('Init B2'); + return fakeImport(ChildB); + }); + + 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...']); + await LazyChildB2; + // We need to flush to trigger the second one to load. + expect(Scheduler).toFlushAndYield(['Init A2', 'Loading...']); + 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 {