@samitouri / QOS-React-2 / commits / 56636353d8

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.

Lee Byron committed Aug 13, 2019 at 18:51 UTC 56636353d8a0bf51c0eee1bbef004c90d6788999
4 files changed +120 -63
packages/react-dom/src/__tests__/ReactServerRendering-test.js
+28
@@ -554,6 +554,34 @@ describe('ReactDOMServer', () => {
554 ),
555 ).not.toThrow();
556 });
557 +
558 + it('renders synchronously resolved lazy component', () => {
559 + const LazyFoo = React.lazy(() => ({
560 + then(resolve) {
561 + resolve({
562 + default: function Foo({id}) {
563 + return <div id={id}>lazy</div>;
564 + },
565 + });
566 + },
567 + }));
568 +
569 + expect(ReactDOMServer.renderToString(<LazyFoo id="foo" />)).toEqual(
570 + '<div id="foo">lazy</div>',
571 + );
572 + });
573 +
574 + it('throws error from synchronously rejected lazy component', () => {
575 + const LazyFoo = React.lazy(() => ({
576 + then(resolve, reject) {
577 + reject(new Error('Bad lazy'));
578 + },
579 + }));
580 +
581 + expect(() => ReactDOMServer.renderToString(<LazyFoo />)).toThrow(
582 + 'Bad lazy',
583 + );
584 + });
585 });
586
587 describe('renderToNodeStream', () => {
packages/react-dom/src/server/ReactPartialRenderer.js
+46 -5
@@ -9,6 +9,7 @@
9
10 import type {ThreadID} from './ReactThreadIDAllocator';
11 import type {ReactElement} from 'shared/ReactElementType';
12 +import type {LazyComponent} from 'shared/ReactLazyComponent';
13 import type {ReactProvider, ReactContext} from 'shared/ReactTypes';
14
15 import React from 'react';
@@ -19,6 +20,12 @@ import warning from 'shared/warning';
20 import warningWithoutStack from 'shared/warningWithoutStack';
21 import describeComponentFrame from 'shared/describeComponentFrame';
22 import ReactSharedInternals from 'shared/ReactSharedInternals';
23 +import {
24 + Resolved,
25 + Rejected,
26 + Pending,
27 + initializeLazyComponentType,
28 +} from 'shared/ReactLazyComponent';
29 import {
30 warnAboutDeprecatedLifecycles,
31 disableLegacyContext,
@@ -1226,11 +1233,45 @@ class ReactDOMServerRenderer {
1233 );
1234 }
1235 // eslint-disable-next-line-no-fallthrough
1229 - case REACT_LAZY_TYPE:
1230 - invariant(
1231 - false,
1232 - 'ReactDOMServer does not yet support lazy-loaded components.',
1233 - );
1236 + case REACT_LAZY_TYPE: {
1237 + const element: ReactElement = (nextChild: any);
1238 + const lazyComponent: LazyComponent<any> = (nextChild: any).type;
1239 + // Attempt to initialize lazy component regardless of whether the
1240 + // suspense server-side renderer is enabled so synchronously
1241 + // resolved constructors are supported.
1242 + initializeLazyComponentType(lazyComponent);
1243 + switch (lazyComponent._status) {
1244 + case Resolved: {
1245 + const nextChildren = [
1246 + React.createElement(
1247 + lazyComponent._result,
1248 + Object.assign({ref: element.ref}, element.props),
1249 + ),
1250 + ];
1251 + const frame: Frame = {
1252 + type: null,
1253 + domNamespace: parentNamespace,
1254 + children: nextChildren,
1255 + childIndex: 0,
1256 + context: context,
1257 + footer: '',
1258 + };
1259 + if (__DEV__) {
1260 + ((frame: any): FrameDev).debugElementStack = [];
1261 + }
1262 + this.stack.push(frame);
1263 + return '';
1264 + }
1265 + case Rejected:
1266 + throw lazyComponent._result;
1267 + case Pending:
1268 + default:
1269 + invariant(
1270 + false,
1271 + 'ReactDOMServer does not yet support lazy-loaded components.',
1272 + );
1273 + }
1274 + }
1275 }
1276 }
1277
packages/react-reconciler/src/ReactFiberLazyComponent.js
+6 -58
@@ -7,10 +7,9 @@
7 * @flow
8 */
9
10 -import type {LazyComponent, Thenable} from 'shared/ReactLazyComponent';
10 +import type {LazyComponent} from 'shared/ReactLazyComponent';
11
12 -import {Resolved, Rejected, Pending} from 'shared/ReactLazyComponent';
13 -import warning from 'shared/warning';
12 +import {Resolved, initializeLazyComponentType} from 'shared/ReactLazyComponent';
13
14 export function resolveDefaultProps(Component: any, baseProps: Object): Object {
15 if (Component && Component.defaultProps) {
@@ -28,60 +27,9 @@ export function resolveDefaultProps(Component: any, baseProps: Object): Object {
27 }
28
29 export function readLazyComponentType<T>(lazyComponent: LazyComponent<T>): T {
31 - const status = lazyComponent._status;
32 - const result = lazyComponent._result;
33 - switch (status) {
34 - case Resolved: {
35 - const Component: T = result;
36 - return Component;
37 - }
38 - case Rejected: {
39 - const error: mixed = result;
40 - throw error;
41 - }
42 - case Pending: {
43 - const thenable: Thenable<T, mixed> = result;
44 - throw thenable;
45 - }
46 - default: {
47 - lazyComponent._status = Pending;
48 - const ctor = lazyComponent._ctor;
49 - const thenable = ctor();
50 - thenable.then(
51 - moduleObject => {
52 - if (lazyComponent._status === Pending) {
53 - const defaultExport = moduleObject.default;
54 - if (__DEV__) {
55 - if (defaultExport === undefined) {
56 - warning(
57 - false,
58 - 'lazy: Expected the result of a dynamic import() call. ' +
59 - 'Instead received: %s\n\nYour code should look like: \n ' +
60 - "const MyComponent = lazy(() => import('./MyComponent'))",
61 - moduleObject,
62 - );
63 - }
64 - }
65 - lazyComponent._status = Resolved;
66 - lazyComponent._result = defaultExport;
67 - }
68 - },
69 - error => {
70 - if (lazyComponent._status === Pending) {
71 - lazyComponent._status = Rejected;
72 - lazyComponent._result = error;
73 - }
74 - },
75 - );
76 - // Handle synchronous thenables.
77 - switch (lazyComponent._status) {
78 - case Resolved:
79 - return lazyComponent._result;
80 - case Rejected:
81 - throw lazyComponent._result;
82 - }
83 - lazyComponent._result = thenable;
84 - throw thenable;
85 - }
30 + initializeLazyComponentType(lazyComponent);
31 + if (lazyComponent._status !== Resolved) {
32 + throw lazyComponent._result;
33 }
34 + return lazyComponent._result;
35 }
packages/shared/ReactLazyComponent.js
+40
@@ -7,6 +7,8 @@
7 * @flow
8 */
9
10 +import warning from 'shared/warning';
11 +
12 export type Thenable<T, R> = {
13 then(resolve: (T) => mixed, reject: (mixed) => mixed): R,
14 };
@@ -25,6 +27,7 @@ type ResolvedLazyComponent<T> = {
27 _result: any,
28 };
29
30 +export const Uninitialized = -1;
31 export const Pending = 0;
32 export const Resolved = 1;
33 export const Rejected = 2;
@@ -34,3 +37,40 @@ export function refineResolvedLazyComponent<T>(
37 ): ResolvedLazyComponent<T> | null {
38 return lazyComponent._status === Resolved ? lazyComponent._result : null;
39 }
40 +
41 +export function initializeLazyComponentType(
42 + lazyComponent: LazyComponent<any>,
43 +): void {
44 + if (lazyComponent._status === Uninitialized) {
45 + lazyComponent._status = Pending;
46 + const ctor = lazyComponent._ctor;
47 + const thenable = ctor();
48 + lazyComponent._result = thenable;
49 + thenable.then(
50 + moduleObject => {
51 + if (lazyComponent._status === Pending) {
52 + const defaultExport = moduleObject.default;
53 + if (__DEV__) {
54 + if (defaultExport === undefined) {
55 + warning(
56 + false,
57 + 'lazy: Expected the result of a dynamic import() call. ' +
58 + 'Instead received: %s\n\nYour code should look like: \n ' +
59 + "const MyComponent = lazy(() => import('./MyComponent'))",
60 + moduleObject,
61 + );
62 + }
63 + }
64 + lazyComponent._status = Resolved;
65 + lazyComponent._result = defaultExport;
66 + }
67 + },
68 + error => {
69 + if (lazyComponent._status === Pending) {
70 + lazyComponent._status = Rejected;
71 + lazyComponent._result = error;
72 + }
73 + },
74 + );
75 + }
76 +}