@samitouri / QOS-React / commits / bd4f056a3e

[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)) {