Skip to content

Commit

Permalink
Partial support for React.lazy() in server renderer. (#16383)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
leebyron authored and sebmarkbage committed Aug 14, 2019
1 parent 6fbe630 commit 5663635
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 63 deletions.
28 changes: 28 additions & 0 deletions packages/react-dom/src/__tests__/ReactServerRendering-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
51 changes: 46 additions & 5 deletions packages/react-dom/src/server/ReactPartialRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down Expand Up @@ -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.',
);
}
}
}
}

Expand Down
64 changes: 6 additions & 58 deletions packages/react-reconciler/src/ReactFiberLazyComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 changes: 40 additions & 0 deletions packages/shared/ReactLazyComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* @flow
*/

import warning from 'shared/warning';

export type Thenable<T, R> = {
then(resolve: (T) => mixed, reject: (mixed) => mixed): R,
};
Expand All @@ -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;
Expand All @@ -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;
}
},
);
}
}

0 comments on commit 5663635

Please sign in to comment.