From 56636353d8a0bf51c0eee1bbef004c90d6788999 Mon Sep 17 00:00:00 2001 From: Lee Byron Date: Tue, 13 Aug 2019 18:51:20 -0700 Subject: [PATCH] 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. --- .../__tests__/ReactServerRendering-test.js | 28 ++++++++ .../src/server/ReactPartialRenderer.js | 51 +++++++++++++-- .../src/ReactFiberLazyComponent.js | 64 ++----------------- packages/shared/ReactLazyComponent.js | 40 ++++++++++++ 4 files changed, 120 insertions(+), 63 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactServerRendering-test.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.js index 8c68b07a5fe88..4cd37d469ad67 100644 --- a/packages/react-dom/src/__tests__/ReactServerRendering-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRendering-test.js @@ -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
lazy
; + }, + }); + }, + })); + + expect(ReactDOMServer.renderToString()).toEqual( + '
lazy
', + ); + }); + + it('throws error from synchronously rejected lazy component', () => { + const LazyFoo = React.lazy(() => ({ + then(resolve, reject) { + reject(new Error('Bad lazy')); + }, + })); + + expect(() => ReactDOMServer.renderToString()).toThrow( + 'Bad lazy', + ); + }); }); describe('renderToNodeStream', () => { diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index 756397eec6690..cab82c611e096 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -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 = (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.', + ); + } + } } } diff --git a/packages/react-reconciler/src/ReactFiberLazyComponent.js b/packages/react-reconciler/src/ReactFiberLazyComponent.js index 1db5ecd35cab2..290458eb96928 100644 --- a/packages/react-reconciler/src/ReactFiberLazyComponent.js +++ b/packages/react-reconciler/src/ReactFiberLazyComponent.js @@ -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(lazyComponent: LazyComponent): 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 = 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; } diff --git a/packages/shared/ReactLazyComponent.js b/packages/shared/ReactLazyComponent.js index f3042ab11aec4..e5bc8b4a37dff 100644 --- a/packages/shared/ReactLazyComponent.js +++ b/packages/shared/ReactLazyComponent.js @@ -7,6 +7,8 @@ * @flow */ +import warning from 'shared/warning'; + export type Thenable = { then(resolve: (T) => mixed, reject: (mixed) => mixed): R, }; @@ -25,6 +27,7 @@ type ResolvedLazyComponent = { _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( ): ResolvedLazyComponent | null { return lazyComponent._status === Resolved ? lazyComponent._result : null; } + +export function initializeLazyComponentType( + lazyComponent: LazyComponent, +): 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; + } + }, + ); + } +}