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
+}