Partial support for React.lazy() in server renderer. (#16383)

Provides partial support for React.lazy() components from the existing PartialRenderer server-side renderer.

Lazy components which are already resolved (or rejected), perhaps with something like `react-ssr-prepass`, can be continued into synchronously. If they have not yet been initialized, they'll be initialized before checking, opening the possibility to exploit this capability with a babel transform. If they're pending (which will typically be the case for a just initialized async ctor) then the existing invariant continues to be thrown.
This commit is contained in:
Lee Byron
2019-08-13 18:51:20 -07:00
committed by Sebastian Markbåge
parent 20ef2666c2
commit 7be0344599
4 changed files with 120 additions and 63 deletions
@@ -554,6 +554,34 @@ describe('ReactDOMServer', () => {
),
).not.toThrow();
});
it('renders synchronously resolved lazy component', () => {
const LazyFoo = React.lazy(() => ({
then(resolve) {
resolve({
default: function Foo({id}) {
return <div id={id}>lazy</div>;
},
});
},
}));
expect(ReactDOMServer.renderToString(<LazyFoo id="foo" />)).toEqual(
'<div id="foo">lazy</div>',
);
});
it('throws error from synchronously rejected lazy component', () => {
const LazyFoo = React.lazy(() => ({
then(resolve, reject) {
reject(new Error('Bad lazy'));
},
}));
expect(() => ReactDOMServer.renderToString(<LazyFoo />)).toThrow(
'Bad lazy',
);
});
});
describe('renderToNodeStream', () => {
+46 -5
View File
@@ -9,6 +9,7 @@
import type {ThreadID} from './ReactThreadIDAllocator';
import type {ReactElement} from 'shared/ReactElementType';
import type {LazyComponent} from 'shared/ReactLazyComponent';
import type {ReactProvider, ReactContext} from 'shared/ReactTypes';
import React from 'react';
@@ -19,6 +20,12 @@ import warning from 'shared/warning';
import warningWithoutStack from 'shared/warningWithoutStack';
import describeComponentFrame from 'shared/describeComponentFrame';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import {
Resolved,
Rejected,
Pending,
initializeLazyComponentType,
} from 'shared/ReactLazyComponent';
import {
warnAboutDeprecatedLifecycles,
disableLegacyContext,
@@ -1226,11 +1233,45 @@ class ReactDOMServerRenderer {
);
}
// eslint-disable-next-line-no-fallthrough
case REACT_LAZY_TYPE:
invariant(
false,
'ReactDOMServer does not yet support lazy-loaded components.',
);
case REACT_LAZY_TYPE: {
const element: ReactElement = (nextChild: any);
const lazyComponent: LazyComponent<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.',
);
}
}
}
}
+6 -58
View File
@@ -7,10 +7,9 @@
* @flow
*/
import type {LazyComponent, Thenable} from 'shared/ReactLazyComponent';
import type {LazyComponent} from 'shared/ReactLazyComponent';
import {Resolved, Rejected, Pending} from 'shared/ReactLazyComponent';
import warning from 'shared/warning';
import {Resolved, initializeLazyComponentType} from 'shared/ReactLazyComponent';
export function resolveDefaultProps(Component: any, baseProps: Object): Object {
if (Component && Component.defaultProps) {
@@ -28,60 +27,9 @@ export function resolveDefaultProps(Component: any, baseProps: Object): Object {
}
export function readLazyComponentType<T>(lazyComponent: LazyComponent<T>): T {
const status = lazyComponent._status;
const result = lazyComponent._result;
switch (status) {
case Resolved: {
const Component: T = result;
return Component;
}
case Rejected: {
const error: mixed = result;
throw error;
}
case Pending: {
const thenable: Thenable<T, mixed> = result;
throw thenable;
}
default: {
lazyComponent._status = Pending;
const ctor = lazyComponent._ctor;
const thenable = ctor();
thenable.then(
moduleObject => {
if (lazyComponent._status === Pending) {
const defaultExport = moduleObject.default;
if (__DEV__) {
if (defaultExport === undefined) {
warning(
false,
'lazy: Expected the result of a dynamic import() call. ' +
'Instead received: %s\n\nYour code should look like: \n ' +
"const MyComponent = lazy(() => import('./MyComponent'))",
moduleObject,
);
}
}
lazyComponent._status = Resolved;
lazyComponent._result = defaultExport;
}
},
error => {
if (lazyComponent._status === Pending) {
lazyComponent._status = Rejected;
lazyComponent._result = error;
}
},
);
// Handle synchronous thenables.
switch (lazyComponent._status) {
case Resolved:
return lazyComponent._result;
case Rejected:
throw lazyComponent._result;
}
lazyComponent._result = thenable;
throw thenable;
}
initializeLazyComponentType(lazyComponent);
if (lazyComponent._status !== Resolved) {
throw lazyComponent._result;
}
return lazyComponent._result;
}
+40
View File
@@ -7,6 +7,8 @@
* @flow
*/
import warning from 'shared/warning';
export type Thenable<T, R> = {
then(resolve: (T) => mixed, reject: (mixed) => mixed): R,
};
@@ -25,6 +27,7 @@ type ResolvedLazyComponent<T> = {
_result: any,
};
export const Uninitialized = -1;
export const Pending = 0;
export const Resolved = 1;
export const Rejected = 2;
@@ -34,3 +37,40 @@ export function refineResolvedLazyComponent<T>(
): ResolvedLazyComponent<T> | null {
return lazyComponent._status === Resolved ? lazyComponent._result : null;
}
export function initializeLazyComponentType(
lazyComponent: LazyComponent<any>,
): void {
if (lazyComponent._status === Uninitialized) {
lazyComponent._status = Pending;
const ctor = lazyComponent._ctor;
const thenable = ctor();
lazyComponent._result = thenable;
thenable.then(
moduleObject => {
if (lazyComponent._status === Pending) {
const defaultExport = moduleObject.default;
if (__DEV__) {
if (defaultExport === undefined) {
warning(
false,
'lazy: Expected the result of a dynamic import() call. ' +
'Instead received: %s\n\nYour code should look like: \n ' +
"const MyComponent = lazy(() => import('./MyComponent'))",
moduleObject,
);
}
}
lazyComponent._status = Resolved;
lazyComponent._result = defaultExport;
}
},
error => {
if (lazyComponent._status === Pending) {
lazyComponent._status = Rejected;
lazyComponent._result = error;
}
},
);
}
}