From da21350462188631a4427302552a6c2a1d9499f3 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 26 Apr 2021 20:31:33 -0400 Subject: [PATCH 1/2] Implement lazy components --- .../src/__tests__/ReactDOMFizzServer-test.js | 132 ++++++++++++++++++ packages/react-server/src/ReactFizzServer.js | 9 +- 2 files changed, 139 insertions(+), 2 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index a42056ab24922..8891ba4c6237f 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -207,6 +207,138 @@ describe('ReactDOMFizzServer', () => { return readText(text); } + // @gate experimental + it('should asynchronously load a lazy component', async () => { + let resolveA; + const LazyA = React.lazy(() => { + return new Promise(r => { + resolveA = r; + }); + }); + + let resolveB; + const LazyB = React.lazy(() => { + return new Promise(r => { + resolveB = r; + }); + }); + + function TextWithPunctuation({text, punctuation}) { + return ; + } + // This tests that default props of the inner element is resolved. + TextWithPunctuation.defaultProps = { + punctuation: '!', + }; + + await act(async () => { + const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable( +
+
+ }> + + +
+
+ }> + + +
+
, + writable, + ); + startWriting(); + }); + expect(getVisibleChildren(container)).toEqual( +
+
Loading...
+
Loading...
+
, + ); + await act(async () => { + resolveA({default: Text}); + }); + expect(getVisibleChildren(container)).toEqual( +
+
Hello
+
Loading...
+
, + ); + await act(async () => { + resolveB({default: TextWithPunctuation}); + }); + expect(getVisibleChildren(container)).toEqual( +
+
Hello
+
world!
+
, + ); + }); + + // @gate experimental + it('should client render a boundary if a lazy component rejects', async () => { + let rejectComponent; + const LazyComponent = React.lazy(() => { + return new Promise((resolve, reject) => { + rejectComponent = reject; + }); + }); + + const loggedErrors = []; + + function App({isClient}) { + return ( +
+ }> + {isClient ? : } + +
+ ); + } + + await act(async () => { + const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable( + , + writable, + { + onError(x) { + loggedErrors.push(x); + }, + }, + ); + startWriting(); + }); + expect(loggedErrors).toEqual([]); + + // Attempt to hydrate the content. + const root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + root.render(); + Scheduler.unstable_flushAll(); + + // We're still loading because we're waiting for the server to stream more content. + expect(getVisibleChildren(container)).toEqual(
Loading...
); + + expect(loggedErrors).toEqual([]); + + const theError = new Error('Test'); + await act(async () => { + rejectComponent(theError); + }); + + expect(loggedErrors).toEqual([theError]); + + // We haven't ran the client hydration yet. + expect(getVisibleChildren(container)).toEqual(
Loading...
); + + // Now we can client render it instead. + Scheduler.unstable_flushAll(); + + // The client rendered HTML is now in place. + expect(getVisibleChildren(container)).toEqual(
Hello
); + + expect(loggedErrors).toEqual([theError]); + }); + // @gate experimental it('should asynchronously load the suspense boundary', async () => { await act(async () => { diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index d51ad44ed64e3..94767646c8633 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -861,10 +861,15 @@ function renderContextProvider( function renderLazyComponent( request: Request, task: Task, - type: LazyComponentType, + lazyComponent: LazyComponentType, props: Object, + ref: any, ): void { - throw new Error('Not yet implemented element type.'); + const payload = lazyComponent._payload; + const init = lazyComponent._init; + const Component = init(payload); + const resolvedProps = resolveDefaultProps(Component, props); + return renderElement(request, task, Component, resolvedProps, ref); } function renderElement( From 2626e3e1550d68f0bcfffa1dd9222c9410cb9ed7 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Mon, 26 Apr 2021 20:43:04 -0400 Subject: [PATCH 2/2] Implement lazy elements / nodes This is used by Flight to encode not yet resolved nodes of any kind. --- .../src/__tests__/ReactDOMFizzServer-test.js | 92 +++++++++++++++++++ packages/react-server/src/ReactFizzServer.js | 13 ++- 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js index 8891ba4c6237f..c2074d80418d2 100644 --- a/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js +++ b/packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js @@ -339,6 +339,98 @@ describe('ReactDOMFizzServer', () => { expect(loggedErrors).toEqual([theError]); }); + // @gate experimental + it('should asynchronously load a lazy element', async () => { + let resolveElement; + const lazyElement = React.lazy(() => { + return new Promise(r => { + resolveElement = r; + }); + }); + + await act(async () => { + const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable( +
+ }> + {lazyElement} + +
, + writable, + ); + startWriting(); + }); + expect(getVisibleChildren(container)).toEqual(
Loading...
); + await act(async () => { + resolveElement({default: }); + }); + expect(getVisibleChildren(container)).toEqual(
Hello
); + }); + + // @gate experimental + it('should client render a boundary if a lazy element rejects', async () => { + let rejectElement; + const element = ; + const lazyElement = React.lazy(() => { + return new Promise((resolve, reject) => { + rejectElement = reject; + }); + }); + + const loggedErrors = []; + + function App({isClient}) { + return ( +
+ }> + {isClient ? element : lazyElement} + +
+ ); + } + + await act(async () => { + const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable( + , + writable, + { + onError(x) { + loggedErrors.push(x); + }, + }, + ); + startWriting(); + }); + expect(loggedErrors).toEqual([]); + + // Attempt to hydrate the content. + const root = ReactDOM.unstable_createRoot(container, {hydrate: true}); + root.render(); + Scheduler.unstable_flushAll(); + + // We're still loading because we're waiting for the server to stream more content. + expect(getVisibleChildren(container)).toEqual(
Loading...
); + + expect(loggedErrors).toEqual([]); + + const theError = new Error('Test'); + await act(async () => { + rejectElement(theError); + }); + + expect(loggedErrors).toEqual([theError]); + + // We haven't ran the client hydration yet. + expect(getVisibleChildren(container)).toEqual(
Loading...
); + + // Now we can client render it instead. + Scheduler.unstable_flushAll(); + + // The client rendered HTML is now in place. + expect(getVisibleChildren(container)).toEqual(
Hello
); + + expect(loggedErrors).toEqual([theError]); + }); + // @gate experimental it('should asynchronously load the suspense boundary', async () => { await act(async () => { diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index 94767646c8633..f272c18b462db 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -102,6 +102,7 @@ import { disableModulePatternComponents, warnAboutDefaultPropsOnFunctionComponents, enableScopeAPI, + enableLazyElements, } from 'shared/ReactFeatureFlags'; import getComponentNameFromType from 'shared/getComponentNameFromType'; @@ -1023,8 +1024,16 @@ function renderNodeDestructive( 'Render them conditionally so that they only appear on the client render.', ); // eslint-disable-next-line-no-fallthrough - case REACT_LAZY_TYPE: - throw new Error('Not yet implemented node type.'); + case REACT_LAZY_TYPE: { + if (enableLazyElements) { + const lazyNode: LazyComponentType = (node: any); + const payload = lazyNode._payload; + const init = lazyNode._init; + const resolvedNode = init(payload); + renderNodeDestructive(request, task, resolvedNode); + return; + } + } } if (isArray(node)) {