Skip to content

Commit bd4f056

Browse files
authored
[Fizz] Implement lazy components and nodes (#21355)
* Implement lazy components * Implement lazy elements / nodes This is used by Flight to encode not yet resolved nodes of any kind.
1 parent a2ae42d commit bd4f056

File tree

2 files changed

+242
-4
lines changed

2 files changed

+242
-4
lines changed

packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,230 @@ describe('ReactDOMFizzServer', () => {
207207
return readText(text);
208208
}
209209

210+
// @gate experimental
211+
it('should asynchronously load a lazy component', async () => {
212+
let resolveA;
213+
const LazyA = React.lazy(() => {
214+
return new Promise(r => {
215+
resolveA = r;
216+
});
217+
});
218+
219+
let resolveB;
220+
const LazyB = React.lazy(() => {
221+
return new Promise(r => {
222+
resolveB = r;
223+
});
224+
});
225+
226+
function TextWithPunctuation({text, punctuation}) {
227+
return <Text text={text + punctuation} />;
228+
}
229+
// This tests that default props of the inner element is resolved.
230+
TextWithPunctuation.defaultProps = {
231+
punctuation: '!',
232+
};
233+
234+
await act(async () => {
235+
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
236+
<div>
237+
<div>
238+
<Suspense fallback={<Text text="Loading..." />}>
239+
<LazyA text="Hello" />
240+
</Suspense>
241+
</div>
242+
<div>
243+
<Suspense fallback={<Text text="Loading..." />}>
244+
<LazyB text="world" />
245+
</Suspense>
246+
</div>
247+
</div>,
248+
writable,
249+
);
250+
startWriting();
251+
});
252+
expect(getVisibleChildren(container)).toEqual(
253+
<div>
254+
<div>Loading...</div>
255+
<div>Loading...</div>
256+
</div>,
257+
);
258+
await act(async () => {
259+
resolveA({default: Text});
260+
});
261+
expect(getVisibleChildren(container)).toEqual(
262+
<div>
263+
<div>Hello</div>
264+
<div>Loading...</div>
265+
</div>,
266+
);
267+
await act(async () => {
268+
resolveB({default: TextWithPunctuation});
269+
});
270+
expect(getVisibleChildren(container)).toEqual(
271+
<div>
272+
<div>Hello</div>
273+
<div>world!</div>
274+
</div>,
275+
);
276+
});
277+
278+
// @gate experimental
279+
it('should client render a boundary if a lazy component rejects', async () => {
280+
let rejectComponent;
281+
const LazyComponent = React.lazy(() => {
282+
return new Promise((resolve, reject) => {
283+
rejectComponent = reject;
284+
});
285+
});
286+
287+
const loggedErrors = [];
288+
289+
function App({isClient}) {
290+
return (
291+
<div>
292+
<Suspense fallback={<Text text="Loading..." />}>
293+
{isClient ? <Text text="Hello" /> : <LazyComponent text="Hello" />}
294+
</Suspense>
295+
</div>
296+
);
297+
}
298+
299+
await act(async () => {
300+
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
301+
<App isClient={false} />,
302+
writable,
303+
{
304+
onError(x) {
305+
loggedErrors.push(x);
306+
},
307+
},
308+
);
309+
startWriting();
310+
});
311+
expect(loggedErrors).toEqual([]);
312+
313+
// Attempt to hydrate the content.
314+
const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
315+
root.render(<App isClient={true} />);
316+
Scheduler.unstable_flushAll();
317+
318+
// We're still loading because we're waiting for the server to stream more content.
319+
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
320+
321+
expect(loggedErrors).toEqual([]);
322+
323+
const theError = new Error('Test');
324+
await act(async () => {
325+
rejectComponent(theError);
326+
});
327+
328+
expect(loggedErrors).toEqual([theError]);
329+
330+
// We haven't ran the client hydration yet.
331+
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
332+
333+
// Now we can client render it instead.
334+
Scheduler.unstable_flushAll();
335+
336+
// The client rendered HTML is now in place.
337+
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
338+
339+
expect(loggedErrors).toEqual([theError]);
340+
});
341+
342+
// @gate experimental
343+
it('should asynchronously load a lazy element', async () => {
344+
let resolveElement;
345+
const lazyElement = React.lazy(() => {
346+
return new Promise(r => {
347+
resolveElement = r;
348+
});
349+
});
350+
351+
await act(async () => {
352+
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
353+
<div>
354+
<Suspense fallback={<Text text="Loading..." />}>
355+
{lazyElement}
356+
</Suspense>
357+
</div>,
358+
writable,
359+
);
360+
startWriting();
361+
});
362+
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
363+
await act(async () => {
364+
resolveElement({default: <Text text="Hello" />});
365+
});
366+
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
367+
});
368+
369+
// @gate experimental
370+
it('should client render a boundary if a lazy element rejects', async () => {
371+
let rejectElement;
372+
const element = <Text text="Hello" />;
373+
const lazyElement = React.lazy(() => {
374+
return new Promise((resolve, reject) => {
375+
rejectElement = reject;
376+
});
377+
});
378+
379+
const loggedErrors = [];
380+
381+
function App({isClient}) {
382+
return (
383+
<div>
384+
<Suspense fallback={<Text text="Loading..." />}>
385+
{isClient ? element : lazyElement}
386+
</Suspense>
387+
</div>
388+
);
389+
}
390+
391+
await act(async () => {
392+
const {startWriting} = ReactDOMFizzServer.pipeToNodeWritable(
393+
<App isClient={false} />,
394+
writable,
395+
{
396+
onError(x) {
397+
loggedErrors.push(x);
398+
},
399+
},
400+
);
401+
startWriting();
402+
});
403+
expect(loggedErrors).toEqual([]);
404+
405+
// Attempt to hydrate the content.
406+
const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
407+
root.render(<App isClient={true} />);
408+
Scheduler.unstable_flushAll();
409+
410+
// We're still loading because we're waiting for the server to stream more content.
411+
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
412+
413+
expect(loggedErrors).toEqual([]);
414+
415+
const theError = new Error('Test');
416+
await act(async () => {
417+
rejectElement(theError);
418+
});
419+
420+
expect(loggedErrors).toEqual([theError]);
421+
422+
// We haven't ran the client hydration yet.
423+
expect(getVisibleChildren(container)).toEqual(<div>Loading...</div>);
424+
425+
// Now we can client render it instead.
426+
Scheduler.unstable_flushAll();
427+
428+
// The client rendered HTML is now in place.
429+
expect(getVisibleChildren(container)).toEqual(<div>Hello</div>);
430+
431+
expect(loggedErrors).toEqual([theError]);
432+
});
433+
210434
// @gate experimental
211435
it('should asynchronously load the suspense boundary', async () => {
212436
await act(async () => {

packages/react-server/src/ReactFizzServer.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ import {
102102
disableModulePatternComponents,
103103
warnAboutDefaultPropsOnFunctionComponents,
104104
enableScopeAPI,
105+
enableLazyElements,
105106
} from 'shared/ReactFeatureFlags';
106107

107108
import getComponentNameFromType from 'shared/getComponentNameFromType';
@@ -861,10 +862,15 @@ function renderContextProvider(
861862
function renderLazyComponent(
862863
request: Request,
863864
task: Task,
864-
type: LazyComponentType<any, any>,
865+
lazyComponent: LazyComponentType<any, any>,
865866
props: Object,
867+
ref: any,
866868
): void {
867-
throw new Error('Not yet implemented element type.');
869+
const payload = lazyComponent._payload;
870+
const init = lazyComponent._init;
871+
const Component = init(payload);
872+
const resolvedProps = resolveDefaultProps(Component, props);
873+
return renderElement(request, task, Component, resolvedProps, ref);
868874
}
869875

870876
function renderElement(
@@ -1018,8 +1024,16 @@ function renderNodeDestructive(
10181024
'Render them conditionally so that they only appear on the client render.',
10191025
);
10201026
// eslint-disable-next-line-no-fallthrough
1021-
case REACT_LAZY_TYPE:
1022-
throw new Error('Not yet implemented node type.');
1027+
case REACT_LAZY_TYPE: {
1028+
if (enableLazyElements) {
1029+
const lazyNode: LazyComponentType<any, any> = (node: any);
1030+
const payload = lazyNode._payload;
1031+
const init = lazyNode._init;
1032+
const resolvedNode = init(payload);
1033+
renderNodeDestructive(request, task, resolvedNode);
1034+
return;
1035+
}
1036+
}
10231037
}
10241038

10251039
if (isArray(node)) {

0 commit comments

Comments
 (0)