Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Fizz] Implement lazy components and nodes #21355

Merged
merged 2 commits into from Apr 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
224 changes: 224 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
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
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