Skip to content

Commit

Permalink
[Fizz] Implement lazy components and nodes (#21355)
Browse files Browse the repository at this point in the history
* Implement lazy components

* Implement lazy elements / nodes

This is used by Flight to encode not yet resolved nodes of any kind.
  • Loading branch information
sebmarkbage authored Apr 27, 2021
1 parent a2ae42d commit bd4f056
Show file tree
Hide file tree
Showing 2 changed files with 242 additions and 4 deletions.
224 changes: 224 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
22 changes: 18 additions & 4 deletions packages/react-server/src/ReactFizzServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ import {
disableModulePatternComponents,
warnAboutDefaultPropsOnFunctionComponents,
enableScopeAPI,
enableLazyElements,
} from 'shared/ReactFeatureFlags';

import getComponentNameFromType from 'shared/getComponentNameFromType';
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)) {
Expand Down

0 comments on commit bd4f056

Please sign in to comment.