diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index ef93c2a02587b..6213f0b72e08e 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -138,6 +138,213 @@ describe('ReactFlight', () => { expect(ReactNoop).toMatchRenderedOutput(Hello, Seb Smith); }); + it('can render a lazy component as a shared component on the server', async () => { + function SharedComponent({text}) { + return ( +
+ shared{text} +
+ ); + } + + 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( +
+ shareda +
, + ); + }); + + 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 ( +
+ shared{text} +
+ ); + } + + let load = null; + + const lazySharedElement = React.lazy(() => { + return new Promise(res => { + load = () => res({default: }); + }); + }); + + function ServerComponent() { + return ( + + {lazySharedElement} + + ); + } + + 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('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
; + } + + 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..e08f308a32fd5 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__) { @@ -477,23 +479,36 @@ 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: { + const payload = (value: any)._payload; + const init = (value: any)._init; + value = init(payload); + 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.