[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.
Sebastian Markbåge committed
Apr 26, 2021 at 21:46 UTC
bd4f056a3ef13ccf3eab4a154b5036a44e0d239d
2 files changed
+242
-4
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+224
@@ -207,6 +207,230 @@ describe('ReactDOMFizzServer', () => {
207
return readText(text);
208
}
209
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
+
434
// @gate experimental
435
it('should asynchronously load the suspense boundary', async () => {
436
await act(async () => {
packages/react-server/src/ReactFizzServer.js
+18
-4
@@ -102,6 +102,7 @@ import {
102
disableModulePatternComponents,
103
warnAboutDefaultPropsOnFunctionComponents,
104
enableScopeAPI,
105
+ enableLazyElements,
106
} from 'shared/ReactFeatureFlags';
107
108
import getComponentNameFromType from 'shared/getComponentNameFromType';
@@ -861,10 +862,15 @@ function renderContextProvider(
862
function renderLazyComponent(
863
request: Request,
864
task: Task,
864
- type: LazyComponentType<any, any>,
865
+ lazyComponent: LazyComponentType<any, any>,
866
props: Object,
867
+ ref: any,
868
): 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);
874
}
875
876
function renderElement(
@@ -1018,8 +1024,16 @@ function renderNodeDestructive(
1024
'Render them conditionally so that they only appear on the client render.',
1025
);
1026
// 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
+ }
1037
}
1038
1039
if (isArray(node)) {