Repository: facebook/react
Pull request: [Fizz] Implement lazy components and nodes

Implements React.lazy either in the component type position (the officially supported use case) and the node position (used by Flight).

This allows code on the server to be async loaded. It doesn't actually transfer any hints about what chunks you might want to eagerly preload like what react-loadable does. That's something you get for free with Server Components but not plain lazy.

Changed files:
filename: packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
patch: @@ -207,6 +207,230 @@ 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 <Text text={text + punctuation} />;
+    }
+    // This tests that default props of the inner element is resolved.
+    TextWithPunctuation.defaultProps = {
+      punctuation: '!',
+    };
+
+    await act(async () => {
+      const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
+        <div>
+          <div>
+            <Suspense fallback={<Text text="Loading..." />}>
+              <LazyA text="Hello" />
+            </Suspense>
+          </div>
+          <div>
+            <Suspense fallback={<Text text="Loading..." />}>
+              <LazyB text="world" />
+            </Suspense>
+          </div>
+        </div>,
+        writable,
+      );
+      startWriting();
+    });
+    expect(getVisibleChildren(container)).toEqual(
+      <div>
+        <div>Loading...</div>
+        <div>Loading...</div>
+      </div>,
+    );
+    await act(async () => {
+      resolveA({default: Text});
+    });
+    expect(getVisibleChildren(container)).toEqual(
+      <div>
+        <div>Hello</div>
+        <div>Loading...</div>
+      </div>,
+    );
+    await act(async () => {
+      resolveB({default: TextWithPunctuation});
+    });
+    expect(getVisibleChildren(container)).toEqual(
+      <div>
+        <div>Hello</div>
+        <div>world!</div>
+      </div>,
+    );
+  });
+
+  // @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 (
+        <div>
+          <Suspense fallback={<Text text="Loading..." />}>
+            {isClient ? <Text text="Hello" /> : <LazyComponent text="Hello" />}
+          </Suspense>
+        </div>
+      );
+    }
+
+    await act(async () => {
+      const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
+        <App isClient={false} />,
+        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(<App isClient={true} />);
+    Scheduler.unstable_flushAll();
+
+    // We're still loading because we're waiting for the server to stream more content.
+    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
+
+    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(<div>Loading...</div>);
+
+    // Now we can client render it instead.
+    Scheduler.unstable_flushAll();
+
+    // The client rendered HTML is now in place.
+    expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
+
+    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(
+        <div>
+          <Suspense fallback={<Text text="Loading..." />}>
+            {lazyElement}
+          </Suspense>
+        </div>,
+        writable,
+      );
+      startWriting();
+    });
+    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
+    await act(async () => {
+      resolveElement({default: <Text text="Hello" />});
+    });
+    expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
+  });
+
+  // @gate experimental
+  it('should client render a boundary if a lazy element rejects', async () => {
+    let rejectElement;
+    const element = <Text text="Hello" />;
+    const lazyElement = React.lazy(() => {
+      return new Promise((resolve, reject) => {
+        rejectElement = reject;
+      });
+    });
+
+    const loggedErrors = [];
+
+    function App({isClient}) {
+      return (
+        <div>
+          <Suspense fallback={<Text text="Loading..." />}>
+            {isClient ? element : lazyElement}
+          </Suspense>
+        </div>
+      );
+    }
+
+    await act(async () => {
+      const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
+        <App isClient={false} />,
+        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(<App isClient={true} />);
+    Scheduler.unstable_flushAll();
+
+    // We're still loading because we're waiting for the server to stream more content.
+    expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
+
+    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(<div>Loading...</div>);
+
+    // Now we can client render it instead.
+    Scheduler.unstable_flushAll();
+
+    // The client rendered HTML is now in place.
+    expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
+
+    expect(loggedErrors).toEqual([theError]);
+  });
+
   // @gate experimental
   it('should asynchronously load the suspense boundary', async () => {
     await act(async () => {

filename: packages/react-server/src/ReactFizzServer.js
patch: @@ -102,6 +102,7 @@ import {
   disableModulePatternComponents,
   warnAboutDefaultPropsOnFunctionComponents,
   enableScopeAPI,
+  enableLazyElements,
 } from 'shared/ReactFeatureFlags';
 
 import getComponentNameFromType from 'shared/getComponentNameFromType';
@@ -861,10 +862,15 @@ function renderContextProvider(
 function renderLazyComponent(
   request: Request,
   task: Task,
-  type: LazyComponentType<any, any>,
+  lazyComponent: LazyComponentType<any, any>,
   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(
@@ -1018,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<any, any> = (node: any);
+          const payload = lazyNode._payload;
+          const init = lazyNode._init;
+          const resolvedNode = init(payload);
+          renderNodeDestructive(request, task, resolvedNode);
+          return;
+        }
+      }
     }
 
     if (isArray(node)) {