From 2e6cdb38304722c0009d41d4b4f37c064476a2cf Mon Sep 17 00:00:00 2001 From: Josh Story Date: Wed, 9 Mar 2022 14:20:00 -0800 Subject: [PATCH 1/3] [Flight] add support for Lazy components in Flight server Lazy components suspend until resolved just like in Fizz. Add tests to confirm Lazy works with Shared Components and Client Component references. --- .../src/__tests__/ReactFlight-test.js | 80 +++++++++++++++++++ .../react-server/src/ReactFlightServer.js | 10 ++- 2 files changed, 86 insertions(+), 4 deletions(-) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index ef93c2a02587b..9a062d89d9e5b 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -138,6 +138,86 @@ describe('ReactFlight', () => { expect(ReactNoop).toMatchRenderedOutput(Hello, Seb Smith); }); + it('can render a lazy component as a shared component on the server', async () => { + function SharedComponent() { + return
I am shared
; + } + + let load = null; + const loadSharedComponent = () => { + return new Promise(res => { + load = () => res({default: SharedComponent}); + }); + }; + + const LazySharedComponent = React.lazy(loadSharedComponent); + + function ServerComponent() { + return ( + + + + ); + } + + const transport = ReactNoopFlightServer.render(); + + act(() => { + const rootModel = ReactNoopFlightClient.read(transport); + ReactNoop.render(rootModel); + }); + expect(ReactNoop).toMatchRenderedOutput('Loading...'); + await load(); + + act(() => { + const rootModel = ReactNoopFlightClient.read(transport); + ReactNoop.render(rootModel); + }); + expect(ReactNoop).toMatchRenderedOutput(
I am shared
); + }); + + it('can render a lazy module reference', async () => { + function ClientComponent() { + return
I am client
; + } + + const ClientComponentReference = moduleReference(ClientComponent); + + let load = null; + const loadClientComponentReference = () => { + return new Promise(res => { + load = () => res({default: ClientComponentReference}); + }); + }; + + const LazyClientComponentReference = React.lazy( + loadClientComponentReference, + ); + + function ServerComponent() { + return ( + + + + ); + } + + const transport = ReactNoopFlightServer.render(); + + act(() => { + const rootModel = ReactNoopFlightClient.read(transport); + ReactNoop.render(rootModel); + }); + expect(ReactNoop).toMatchRenderedOutput('Loading...'); + await load(); + + act(() => { + const rootModel = ReactNoopFlightClient.read(transport); + ReactNoop.render(rootModel); + }); + expect(ReactNoop).toMatchRenderedOutput(
I am client
); + }); + it('should error if a non-serializable value is passed to a host component', () => { function EventHandlerProp() { return ( diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 32a08b1eff812..68193cdad3827 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -200,6 +200,12 @@ function attemptResolveElement( return [REACT_ELEMENT_TYPE, type, key, props]; } switch (type.$$typeof) { + case REACT_LAZY_TYPE: { + const payload = type._payload; + const init = type._init; + const wrappedType = init(payload); + return attemptResolveElement(wrappedType, key, ref, props); + } case REACT_FORWARD_REF_TYPE: { const render = type.render; return render(props, undefined); @@ -452,10 +458,6 @@ export function resolveModelToJSON( switch (value) { case REACT_ELEMENT_TYPE: return '$'; - case REACT_LAZY_TYPE: - throw new Error( - 'React Lazy Components are not yet supported on the server.', - ); } if (__DEV__) { From 6b9c8c69b35e58aa6f3efa8a9073c1defb60b81c Mon Sep 17 00:00:00 2001 From: Josh Story Date: Thu, 10 Mar 2022 00:47:14 -0800 Subject: [PATCH 2/3] Support Lazy elements React.Lazy can now return an element instead of a Component. This commit implements support for Lazy elements when server rendering. --- .../src/__tests__/ReactFlight-test.js | 61 +++++++++++++++++-- .../react-server/src/ReactFlightServer.js | 36 ++++++++--- 2 files changed, 83 insertions(+), 14 deletions(-) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 9a062d89d9e5b..df8476130e32a 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -139,8 +139,12 @@ describe('ReactFlight', () => { }); it('can render a lazy component as a shared component on the server', async () => { - function SharedComponent() { - return
I am shared
; + function SharedComponent({text}) { + return ( +
+ shared{text} +
+ ); } let load = null; @@ -155,7 +159,52 @@ describe('ReactFlight', () => { function ServerComponent() { return ( - + + + ); + } + + const transport = ReactNoopFlightServer.render(); + + act(() => { + const rootModel = ReactNoopFlightClient.read(transport); + ReactNoop.render(rootModel); + }); + expect(ReactNoop).toMatchRenderedOutput('Loading...'); + await load(); + + act(() => { + const rootModel = ReactNoopFlightClient.read(transport); + ReactNoop.render(rootModel); + }); + expect(ReactNoop).toMatchRenderedOutput( +
+ shareda +
, + ); + }); + + it('can render a lazy element', async () => { + function SharedComponent({text}) { + return ( +
+ shared{text} +
+ ); + } + + let load = null; + + const lazySharedElement = React.lazy(() => { + return new Promise(res => { + load = () => res({default: }); + }); + }); + + function ServerComponent() { + return ( + + {lazySharedElement} ); } @@ -173,7 +222,11 @@ describe('ReactFlight', () => { const rootModel = ReactNoopFlightClient.read(transport); ReactNoop.render(rootModel); }); - expect(ReactNoop).toMatchRenderedOutput(
I am shared
); + expect(ReactNoop).toMatchRenderedOutput( +
+ shareda +
, + ); }); it('can render a lazy module reference', async () => { diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 68193cdad3827..7ea3e1ef83894 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -210,6 +210,11 @@ function attemptResolveElement( const render = type.render; return render(props, undefined); } + case REACT_ELEMENT_TYPE: { + // this can happen when a lazy component resolves to an element instead of + // a Component. + return attemptResolveElement(type.type, type.key, type.ref, type.props); + } case REACT_MEMO_TYPE: { return attemptResolveElement(type.type, key, ref, props); } @@ -479,23 +484,34 @@ export function resolveModelToJSON( while ( typeof value === 'object' && value !== null && - (value: any).$$typeof === REACT_ELEMENT_TYPE + ((value: any).$$typeof === REACT_ELEMENT_TYPE || + (value: any).$$typeof === REACT_LAZY_TYPE) ) { if (__DEV__) { if (isInsideContextValue) { console.error('React elements are not allowed in ServerContext'); } } - // TODO: Concatenate keys of parents onto children. - const element: React$Element = (value: any); + try { - // Attempt to render the server component. - value = attemptResolveElement( - element.type, - element.key, - element.ref, - element.props, - ); + switch ((value: any).$$typeof) { + case REACT_ELEMENT_TYPE: { + // TODO: Concatenate keys of parents onto children. + const element: React$Element = (value: any); + // Attempt to render the server component. + value = attemptResolveElement( + element.type, + element.key, + element.ref, + element.props, + ); + break; + } + case REACT_LAZY_TYPE: { + value = attemptResolveElement(value, null, null, {}); + break; + } + } } catch (x) { if (typeof x === 'object' && x !== null && typeof x.then === 'function') { // Something suspended, we'll need to create a new segment and resolve it later. From a4329f4bf8065183c813bff9a1ca46f5b596441b Mon Sep 17 00:00:00 2001 From: Josh Story Date: Thu, 10 Mar 2022 10:55:41 -0800 Subject: [PATCH 3/3] add lazy initialization to resolveModelToJson adding lazying initialization toResolveModelToJson means we use attemptResolveElement's full logic on whatever the resolved type ends up being. This better aligns handling of misued Lazy types like a lazy element being used as a Component or a lazy Component being used as an element. --- .../src/__tests__/ReactFlight-test.js | 74 +++++++++++++++++++ .../react-server/src/ReactFlightServer.js | 9 +-- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index df8476130e32a..6213f0b72e08e 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -184,6 +184,43 @@ describe('ReactFlight', () => { ); }); + it('errors on a Lazy element being used in Component position', async () => { + function SharedComponent({text}) { + return ( +
+ shared{text} +
+ ); + } + + let load = null; + + const LazyElementDisguisedAsComponent = React.lazy(() => { + return new Promise(res => { + load = () => res({default: }); + }); + }); + + function ServerComponent() { + return ( + + + + ); + } + + const transport = ReactNoopFlightServer.render(); + + act(() => { + const rootModel = ReactNoopFlightClient.read(transport); + ReactNoop.render(rootModel); + }); + expect(ReactNoop).toMatchRenderedOutput('Loading...'); + spyOnDevAndProd(console, 'error'); + await load(); + expect(console.error).toHaveBeenCalledTimes(1); + }); + it('can render a lazy element', async () => { function SharedComponent({text}) { return ( @@ -229,6 +266,43 @@ describe('ReactFlight', () => { ); }); + it('errors with lazy value in element position that resolves to Component', async () => { + function SharedComponent({text}) { + return ( +
+ shared{text} +
+ ); + } + + let load = null; + + const componentDisguisedAsElement = React.lazy(() => { + return new Promise(res => { + load = () => res({default: SharedComponent}); + }); + }); + + function ServerComponent() { + return ( + + {componentDisguisedAsElement} + + ); + } + + const transport = ReactNoopFlightServer.render(); + + act(() => { + const rootModel = ReactNoopFlightClient.read(transport); + ReactNoop.render(rootModel); + }); + expect(ReactNoop).toMatchRenderedOutput('Loading...'); + spyOnDevAndProd(console, 'error'); + await load(); + expect(console.error).toHaveBeenCalledTimes(1); + }); + it('can render a lazy module reference', async () => { function ClientComponent() { return
I am client
; diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 7ea3e1ef83894..e08f308a32fd5 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -210,11 +210,6 @@ function attemptResolveElement( const render = type.render; return render(props, undefined); } - case REACT_ELEMENT_TYPE: { - // this can happen when a lazy component resolves to an element instead of - // a Component. - return attemptResolveElement(type.type, type.key, type.ref, type.props); - } case REACT_MEMO_TYPE: { return attemptResolveElement(type.type, key, ref, props); } @@ -508,7 +503,9 @@ export function resolveModelToJSON( break; } case REACT_LAZY_TYPE: { - value = attemptResolveElement(value, null, null, {}); + const payload = (value: any)._payload; + const init = (value: any)._init; + value = init(payload); break; } }