Skip to content

Refactor Lazy Components to use teh Suspense (and wrap Blocks in Lazy) #18362

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Mar 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ describe('ReactDOMServer', () => {
),
);
ReactDOMServer.renderToString(<LazyFoo />);
}).toThrow('ReactDOMServer does not yet support lazy-loaded components.');
}).toThrow('ReactDOMServer does not yet support Suspense.');
});

it('throws when suspending on the server', () => {
Expand Down
57 changes: 23 additions & 34 deletions packages/react-dom/src/server/ReactPartialRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ import invariant from 'shared/invariant';
import getComponentName from 'shared/getComponentName';
import describeComponentFrame from 'shared/describeComponentFrame';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {initializeLazyComponentType} from 'shared/ReactLazyComponent';
import {Resolved, Rejected, Pending} from 'shared/ReactLazyStatusTags';
import {
warnAboutDeprecatedLifecycles,
disableLegacyContext,
Expand Down Expand Up @@ -1233,42 +1231,33 @@ class ReactDOMServerRenderer {
// eslint-disable-next-line-no-fallthrough
case REACT_LAZY_TYPE: {
const element: ReactElement = (nextChild: any);
const lazyComponent: LazyComponent<any> = (nextChild: any).type;
const lazyComponent: LazyComponent<any, any> = (nextChild: any)
.type;
// Attempt to initialize lazy component regardless of whether the
// suspense server-side renderer is enabled so synchronously
// resolved constructors are supported.
initializeLazyComponentType(lazyComponent);
switch (lazyComponent._status) {
case Resolved: {
const nextChildren = [
React.createElement(
lazyComponent._result,
Object.assign({ref: element.ref}, element.props),
),
];
const frame: Frame = {
type: null,
domNamespace: parentNamespace,
children: nextChildren,
childIndex: 0,
context: context,
footer: '',
};
if (__DEV__) {
((frame: any): FrameDev).debugElementStack = [];
}
this.stack.push(frame);
return '';
}
case Rejected:
throw lazyComponent._result;
case Pending:
default:
invariant(
false,
'ReactDOMServer does not yet support lazy-loaded components.',
);
let payload = lazyComponent._payload;
let init = lazyComponent._init;
let result = init(payload);
const nextChildren = [
React.createElement(
result,
Object.assign({ref: element.ref}, element.props),
),
];
const frame: Frame = {
type: null,
domNamespace: parentNamespace,
children: nextChildren,
childIndex: 0,
context: context,
footer: '',
};
if (__DEV__) {
((frame: any): FrameDev).debugElementStack = [];
}
this.stack.push(frame);
return '';
}
// eslint-disable-next-line-no-fallthrough
case REACT_SCOPE_TYPE: {
Expand Down
50 changes: 35 additions & 15 deletions packages/react-reconciler/src/ReactChildFiber.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import type {ReactElement} from 'shared/ReactElementType';
import type {ReactPortal} from 'shared/ReactTypes';
import type {BlockComponent} from 'react/src/ReactBlock';
import type {LazyComponent} from 'react/src/ReactLazy';
import type {Fiber} from './ReactFiber';
import type {ExpirationTime} from './ReactFiberExpirationTime';

Expand All @@ -20,6 +21,7 @@ import {
REACT_ELEMENT_TYPE,
REACT_FRAGMENT_TYPE,
REACT_PORTAL_TYPE,
REACT_LAZY_TYPE,
REACT_BLOCK_TYPE,
} from 'shared/ReactSymbols';
import {
Expand Down Expand Up @@ -48,7 +50,6 @@ import {
} from './ReactCurrentFiber';
import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading';
import {StrictMode} from './ReactTypeOfMode';
import {initializeBlockComponentType} from 'shared/ReactLazyComponent';

let didWarnAboutMaps;
let didWarnAboutGenerators;
Expand Down Expand Up @@ -263,6 +264,22 @@ function warnOnFunctionType() {
}
}

// We avoid inlining this to avoid potential deopts from using try/catch.
/** @noinline */
function resolveLazyType<T, P>(
lazyComponent: LazyComponent<T, P>,
): LazyComponent<T, P> | T {
try {
// If we can, let's peek at the resulting type.
let payload = lazyComponent._payload;
let init = lazyComponent._init;
return init(payload);
} catch (x) {
// Leave it in place and let it throw again in the begin phase.
return lazyComponent;
}
}

// 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 @@ -419,22 +436,22 @@ function ChildReconciler(shouldTrackSideEffects) {
existing._debugOwner = element._owner;
}
return existing;
} else if (
enableBlocksAPI &&
current.tag === Block &&
element.type.$$typeof === REACT_BLOCK_TYPE
) {
} else if (enableBlocksAPI && current.tag === Block) {
// The new Block might not be initialized yet. We need to initialize
// it in case initializing it turns out it would match.
initializeBlockComponentType(element.type);
let type = element.type;
if (type.$$typeof === REACT_LAZY_TYPE) {
type = resolveLazyType(type);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This got lost when we dropped Blocks. The benefit of Blocks was that we only needed this check if the tag was Block but now we need to do it for everything.

}
if (
(element.type: BlockComponent<any, any, any>)._fn ===
(current.type: BlockComponent<any, any, any>)._fn
type.$$typeof === REACT_BLOCK_TYPE &&
((type: any): BlockComponent<any, any>)._render ===
(current.type: BlockComponent<any, any>)._render
) {
// Same as above but also update the .type field.
const existing = useFiber(current, element.props);
existing.return = returnFiber;
existing.type = element.type;
existing.type = type;
if (__DEV__) {
existing._debugSource = element._source;
existing._debugOwner = element._owner;
Expand Down Expand Up @@ -1188,17 +1205,20 @@ function ChildReconciler(shouldTrackSideEffects) {
}
case Block:
if (enableBlocksAPI) {
if (element.type.$$typeof === REACT_BLOCK_TYPE) {
let type = element.type;
if (type.$$typeof === REACT_LAZY_TYPE) {
type = resolveLazyType(type);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For updates, we attempt to initialize earlier to see if we can reconcile against the inner value. I only do this for Blocks right now to preserve backwards compatibility.

However, in non-legacy mode, we could reconcile against the resolved value of any type.

E.g. if I have two lazy components that resolve to the same thing, why shouldn't they reconcile against each other?

}
if (type.$$typeof === REACT_BLOCK_TYPE) {
// The new Block might not be initialized yet. We need to initialize
// it in case initializing it turns out it would match.
initializeBlockComponentType(element.type);
if (
(element.type: BlockComponent<any, any, any>)._fn ===
(child.type: BlockComponent<any, any, any>)._fn
((type: any): BlockComponent<any, any>)._render ===
(child.type: BlockComponent<any, any>)._render
) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.type = element.type;
existing.type = type;
existing.return = returnFiber;
if (__DEV__) {
existing._debugSource = element._source;
Expand Down
34 changes: 17 additions & 17 deletions packages/react-reconciler/src/ReactFiberBeginWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import type {ReactProviderType, ReactContext} from 'shared/ReactTypes';
import type {BlockComponent} from 'react/src/ReactBlock';
import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
import type {Fiber} from './ReactFiber';
import type {FiberRoot} from './ReactFiberRoot';
import type {ExpirationTime} from './ReactFiberExpirationTime';
Expand Down Expand Up @@ -73,7 +74,6 @@ import invariant from 'shared/invariant';
import shallowEqual from 'shared/shallowEqual';
import getComponentName from 'shared/getComponentName';
import ReactStrictModeWarnings from './ReactStrictModeWarnings';
import {refineResolvedLazyComponent} from 'shared/ReactLazyComponent';
import {REACT_LAZY_TYPE, getIteratorFn} from 'shared/ReactSymbols';
import {
getCurrentFiberOwnerNameInDevOrNull,
Expand Down Expand Up @@ -164,11 +164,7 @@ import {
resumeMountClassInstance,
updateClassInstance,
} from './ReactFiberClassComponent';
import {
readLazyComponentType,
resolveDefaultProps,
} from './ReactFiberLazyComponent';
import {initializeBlockComponentType} from 'shared/ReactLazyComponent';
import {resolveDefaultProps} from './ReactFiberLazyComponent';
import {
resolveLazyComponentTag,
createFiberFromTypeAndProps,
Expand All @@ -184,7 +180,6 @@ import {
renderDidSuspendDelayIfPossible,
markUnprocessedUpdateTime,
} from './ReactFiberWorkLoop';
import {Resolved} from 'shared/ReactLazyStatusTags';

const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;

Expand Down Expand Up @@ -492,7 +487,14 @@ function updateSimpleMemoComponent(
// We warn when you define propTypes on lazy()
// so let's just skip over it to find memo() outer wrapper.
// Inner props for memo are validated later.
outerMemoType = refineResolvedLazyComponent(outerMemoType);
const lazyComponent: LazyComponentType<any, any> = outerMemoType;
let payload = lazyComponent._payload;
let init = lazyComponent._init;
try {
outerMemoType = init(payload);
} catch (x) {
outerMemoType = null;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just let it throw? If we got here, doesn't mean it's resolved anyway?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. I think you’re right. I think that we’d only get here if the init is not deterministic. I.e. if it resuspends. That’s a bug in the lazy code but could possibly happen as a bug in a “user provided lazy”.

That would be a prod changing behavior so probably best to keep this since it wouldn’t happen in prod (although the call wouldn’t happen neither).

}
}
const outerPropTypes = outerMemoType && (outerMemoType: any).propTypes;
if (outerPropTypes) {
Expand Down Expand Up @@ -703,23 +705,18 @@ function updateFunctionComponent(
return workInProgress.child;
}

function updateBlock<Props, Payload, Data>(
function updateBlock<Props, Data>(
current: Fiber | null,
workInProgress: Fiber,
block: BlockComponent<Props, Payload, Data>,
block: BlockComponent<Props, Data>,
nextProps: any,
renderExpirationTime: ExpirationTime,
) {
// TODO: current can be non-null here even if the component
// hasn't yet mounted. This happens after the first render suspends.
// We'll need to figure out if this is fine or can cause issues.

initializeBlockComponentType(block);
if (block._status !== Resolved) {
throw block._data;
}

const render = block._fn;
const render = block._render;
const data = block._data;

// The rest is a fork of updateFunctionComponent
Expand Down Expand Up @@ -1142,7 +1139,10 @@ function mountLazyComponent(
// We can't start a User Timing measurement with correct label yet.
// Cancel and resume right after we know the tag.
cancelWorkTimer(workInProgress);
let Component = readLazyComponentType(elementType);
let lazyComponent: LazyComponentType<any, any> = elementType;
let payload = lazyComponent._payload;
let init = lazyComponent._init;
let Component = init(payload);
// Store the unwrapped component in the type.
workInProgress.type = Component;
const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component));
Expand Down
13 changes: 0 additions & 13 deletions packages/react-reconciler/src/ReactFiberLazyComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@
* @flow
*/

import type {LazyComponent} from 'react/src/ReactLazy';

import {Resolved} from 'shared/ReactLazyStatusTags';
import {initializeLazyComponentType} from 'shared/ReactLazyComponent';

export function resolveDefaultProps(Component: any, baseProps: Object): Object {
if (Component && Component.defaultProps) {
// Resolve default props. Taken from ReactElement
Expand All @@ -26,11 +21,3 @@ export function resolveDefaultProps(Component: any, baseProps: Object): Object {
}
return baseProps;
}

export function readLazyComponentType<T>(lazyComponent: LazyComponent<T>): T {
initializeLazyComponentType(lazyComponent);
if (lazyComponent._status !== Resolved) {
throw lazyComponent._result;
}
return lazyComponent._result;
}
Loading