Skip to content

Commit

Permalink
Reconcile element types of lazy component yielding the same type
Browse files Browse the repository at this point in the history
  • Loading branch information
sebmarkbage committed Dec 1, 2020
1 parent 3f73dce commit ffe59a8
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 98 deletions.
102 changes: 53 additions & 49 deletions packages/react-reconciler/src/ReactChildFiber.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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.
Expand Down
102 changes: 53 additions & 49 deletions packages/react-reconciler/src/ReactChildFiber.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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.
Expand Down
87 changes: 87 additions & 0 deletions packages/react-reconciler/src/__tests__/ReactLazy-test.internal.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Text text={this.props.label} />;
}
}

function ChildA({lowerCase}) {
return <Child label={lowerCase ? 'a' : 'A'} />;
}

function ChildB({lowerCase}) {
return <Child label={lowerCase ? 'b' : 'B'} />;
}

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 (
<Suspense fallback={<Text text="Loading..." />}>
{swap
? [
<LazyChildB2 key="B" lowerCase={true} />,
<LazyChildA2 key="A" lowerCase={true} />,
]
: [<LazyChildA key="A" />, <LazyChildB key="B" />]}
</Suspense>
);
}

const root = ReactTestRenderer.create(<Parent swap={false} />, {
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(<Parent swap={true} />);
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 {
Expand Down

0 comments on commit ffe59a8

Please sign in to comment.