main
js 3,518 lines 109 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @flow
8 */
9
10 let React;
11 let ReactNoop;
12 let Scheduler;
13 let act;
14 let Suspense;
15 let getCacheForType;
16 let caches;
17 let seededCache;
18 let ErrorBoundary;
19 let waitForAll;
20 let waitFor;
21 let assertLog;
22
23 // TODO: These tests don't pass in persistent mode yet. Need to implement.
24
25 describe('ReactSuspenseEffectsSemantics', () => {
26 beforeEach(() => {
27 jest.resetModules();
28
29 React = require('react');
30 ReactNoop = require('react-noop-renderer');
31 Scheduler = require('scheduler');
32 act = require('internal-test-utils').act;
33 Suspense = React.Suspense;
34
35 getCacheForType = React.unstable_getCacheForType;
36
37 const InternalTestUtils = require('internal-test-utils');
38 waitForAll = InternalTestUtils.waitForAll;
39 waitFor = InternalTestUtils.waitFor;
40 assertLog = InternalTestUtils.assertLog;
41
42 caches = [];
43 seededCache = null;
44
45 ErrorBoundary = class extends React.Component {
46 state = {error: null};
47 componentDidCatch(error) {
48 this.setState({error});
49 }
50 render() {
51 if (this.state.error) {
52 Scheduler.log('ErrorBoundary render: catch');
53 return this.props.fallback;
54 }
55 Scheduler.log('ErrorBoundary render: try');
56 return this.props.children;
57 }
58 };
59 });
60
61 function createTextCache() {
62 if (seededCache !== null) {
63 // Trick to seed a cache before it exists.
64 // TODO: Need a built-in API to seed data before the initial render (i.e.
65 // not a refresh because nothing has mounted yet).
66 const cache = seededCache;
67 seededCache = null;
68 return cache;
69 }
70
71 const data = new Map();
72 const version = caches.length + 1;
73 const cache = {
74 version,
75 data,
76 resolve(text) {
77 const record = data.get(text);
78 if (record === undefined) {
79 const newRecord = {
80 status: 'resolved',
81 value: text,
82 };
83 data.set(text, newRecord);
84 } else if (record.status === 'pending') {
85 const thenable = record.value;
86 record.status = 'resolved';
87 record.value = text;
88 thenable.pings.forEach(t => t());
89 }
90 },
91 reject(text, error) {
92 const record = data.get(text);
93 if (record === undefined) {
94 const newRecord = {
95 status: 'rejected',
96 value: error,
97 };
98 data.set(text, newRecord);
99 } else if (record.status === 'pending') {
100 const thenable = record.value;
101 record.status = 'rejected';
102 record.value = error;
103 thenable.pings.forEach(t => t());
104 }
105 },
106 };
107 caches.push(cache);
108 return cache;
109 }
110
111 function readText(text) {
112 const textCache = getCacheForType(createTextCache);
113 const record = textCache.data.get(text);
114 if (record !== undefined) {
115 switch (record.status) {
116 case 'pending':
117 Scheduler.log(`Suspend:${text}`);
118 throw record.value;
119 case 'rejected':
120 Scheduler.log(`Error:${text}`);
121 throw record.value;
122 case 'resolved':
123 return textCache.version;
124 }
125 } else {
126 Scheduler.log(`Suspend:${text}`);
127
128 const thenable = {
129 pings: [],
130 then(resolve) {
131 if (newRecord.status === 'pending') {
132 thenable.pings.push(resolve);
133 } else {
134 Promise.resolve().then(() => resolve(newRecord.value));
135 }
136 },
137 };
138
139 const newRecord = {
140 status: 'pending',
141 value: thenable,
142 };
143 textCache.data.set(text, newRecord);
144
145 throw thenable;
146 }
147 }
148
149 function Text({children = null, text}) {
150 Scheduler.log(`Text:${text} render`);
151 React.useInsertionEffect(() => {
152 Scheduler.log(`Text:${text} create insertion`);
153 return () => {
154 Scheduler.log(`Text:${text} destroy insertion`);
155 };
156 }, []);
157
158 React.useLayoutEffect(() => {
159 Scheduler.log(`Text:${text} create layout`);
160 return () => {
161 Scheduler.log(`Text:${text} destroy layout`);
162 };
163 }, []);
164 React.useEffect(() => {
165 Scheduler.log(`Text:${text} create passive`);
166 return () => {
167 Scheduler.log(`Text:${text} destroy passive`);
168 };
169 }, []);
170 return <span prop={text}>{children}</span>;
171 }
172
173 function AsyncText({children = null, text}) {
174 readText(text);
175 Scheduler.log(`AsyncText:${text} render`);
176 React.useLayoutEffect(() => {
177 Scheduler.log(`AsyncText:${text} create layout`);
178 return () => {
179 Scheduler.log(`AsyncText:${text} destroy layout`);
180 };
181 }, []);
182 React.useEffect(() => {
183 Scheduler.log(`AsyncText:${text} create passive`);
184 return () => {
185 Scheduler.log(`AsyncText:${text} destroy passive`);
186 };
187 }, []);
188 return <span prop={text}>{children}</span>;
189 }
190
191 function resolveMostRecentTextCache(text) {
192 if (caches.length === 0) {
193 throw Error('Cache does not exist.');
194 } else {
195 // Resolve the most recently created cache. An older cache can by
196 // resolved with `caches[index].resolve(text)`.
197 caches[caches.length - 1].resolve(text);
198 }
199 }
200
201 const resolveText = resolveMostRecentTextCache;
202
203 function advanceTimers(ms) {
204 // Note: This advances Jest's virtual time but not React's. Use
205 // ReactNoop.expire for that.
206 if (typeof ms !== 'number') {
207 throw new Error('Must specify ms');
208 }
209 jest.advanceTimersByTime(ms);
210 // Wait until the end of the current tick
211 // We cannot use a timer since we're faking them
212 return Promise.resolve().then(() => {});
213 }
214
215 describe('when a component suspends during initial mount', () => {
216 // @gate enableLegacyCache
217 it('should not change behavior in concurrent mode', async () => {
218 class ClassText extends React.Component {
219 componentDidMount() {
220 const {text} = this.props;
221 Scheduler.log(`ClassText:${text} componentDidMount`);
222 }
223 componentDidUpdate() {
224 const {text} = this.props;
225 Scheduler.log(`ClassText:${text} componentDidUpdate`);
226 }
227 componentWillUnmount() {
228 const {text} = this.props;
229 Scheduler.log(`ClassText:${text} componentWillUnmount`);
230 }
231 render() {
232 const {children, text} = this.props;
233 Scheduler.log(`ClassText:${text} render`);
234 return <span prop={text}>{children}</span>;
235 }
236 }
237
238 function App({children = null}) {
239 Scheduler.log('App render');
240 React.useLayoutEffect(() => {
241 Scheduler.log('App create layout');
242 return () => {
243 Scheduler.log('App destroy layout');
244 };
245 }, []);
246 React.useEffect(() => {
247 Scheduler.log('App create passive');
248 return () => {
249 Scheduler.log('App destroy passive');
250 };
251 }, []);
252 return (
253 <>
254 <Suspense fallback={<Text text="Fallback" />}>
255 <Text text="Inside:Before" />
256 {children}
257 <ClassText text="Inside:After" />
258 </Suspense>
259 <Text text="Outside" />
260 </>
261 );
262 }
263
264 // Mount and suspend.
265 await act(() => {
266 ReactNoop.render(
267 <App>
268 <AsyncText text="Async" />
269 </App>,
270 );
271 });
272 assertLog([
273 'App render',
274 'Text:Inside:Before render',
275 'Suspend:Async',
276 'Text:Fallback render',
277 'Text:Outside render',
278 'Text:Fallback create insertion',
279 'Text:Outside create insertion',
280 'Text:Fallback create layout',
281 'Text:Outside create layout',
282 'App create layout',
283 'Text:Fallback create passive',
284 'Text:Outside create passive',
285 'App create passive',
286 // pre-warming
287 'Text:Inside:Before render',
288 'Suspend:Async',
289 'ClassText:Inside:After render',
290 ]);
291 expect(ReactNoop).toMatchRenderedOutput(
292 <>
293 <span prop="Fallback" />
294 <span prop="Outside" />
295 </>,
296 );
297
298 // Resolving the suspended resource should
299 await act(async () => {
300 await resolveText('Async');
301 });
302 assertLog([
303 'Text:Inside:Before render',
304 'AsyncText:Async render',
305 'ClassText:Inside:After render',
306 'Text:Fallback destroy insertion',
307 'Text:Fallback destroy layout',
308 'Text:Inside:Before create insertion',
309 'Text:Inside:Before create layout',
310 'AsyncText:Async create layout',
311 'ClassText:Inside:After componentDidMount',
312 'Text:Fallback destroy passive',
313 'Text:Inside:Before create passive',
314 'AsyncText:Async create passive',
315 ]);
316 expect(ReactNoop).toMatchRenderedOutput(
317 <>
318 <span prop="Inside:Before" />
319 <span prop="Async" />
320 <span prop="Inside:After" />
321 <span prop="Outside" />
322 </>,
323 );
324
325 await act(() => {
326 ReactNoop.render(null);
327 });
328 assertLog([
329 'App destroy layout',
330 'Text:Inside:Before destroy insertion',
331 'Text:Inside:Before destroy layout',
332 'AsyncText:Async destroy layout',
333 'ClassText:Inside:After componentWillUnmount',
334 'Text:Outside destroy insertion',
335 'Text:Outside destroy layout',
336 'App destroy passive',
337 'Text:Inside:Before destroy passive',
338 'AsyncText:Async destroy passive',
339 'Text:Outside destroy passive',
340 ]);
341 expect(ReactNoop).toMatchRenderedOutput(null);
342 });
343
344 // @gate enableLegacyCache && !disableLegacyMode
345 it('should not change behavior in sync', async () => {
346 class ClassText extends React.Component {
347 componentDidMount() {
348 const {text} = this.props;
349 Scheduler.log(`ClassText:${text} componentDidMount`);
350 }
351 componentDidUpdate() {
352 const {text} = this.props;
353 Scheduler.log(`ClassText:${text} componentDidUpdate`);
354 }
355 componentWillUnmount() {
356 const {text} = this.props;
357 Scheduler.log(`ClassText:${text} componentWillUnmount`);
358 }
359 render() {
360 const {children, text} = this.props;
361 Scheduler.log(`ClassText:${text} render`);
362 return <span prop={text}>{children}</span>;
363 }
364 }
365
366 function App({children = null}) {
367 Scheduler.log('App render');
368 React.useLayoutEffect(() => {
369 Scheduler.log('App create layout');
370 return () => {
371 Scheduler.log('App destroy layout');
372 };
373 }, []);
374 React.useEffect(() => {
375 Scheduler.log('App create passive');
376 return () => {
377 Scheduler.log('App destroy passive');
378 };
379 }, []);
380 return (
381 <>
382 <Suspense fallback={<Text text="Fallback" />}>
383 <Text text="Inside:Before" />
384 {children}
385 <ClassText text="Inside:After" />
386 </Suspense>
387 <Text text="Outside" />
388 </>
389 );
390 }
391
392 // Mount and suspend.
393 await act(() => {
394 ReactNoop.renderLegacySyncRoot(
395 <App>
396 <AsyncText text="Async" />
397 </App>,
398 );
399 });
400 assertLog([
401 'App render',
402 'Text:Inside:Before render',
403 'Suspend:Async',
404 'ClassText:Inside:After render',
405 'Text:Fallback render',
406 'Text:Outside render',
407 'Text:Inside:Before create insertion',
408 'Text:Fallback create insertion',
409 'Text:Outside create insertion',
410 'Text:Inside:Before create layout',
411 'ClassText:Inside:After componentDidMount',
412 'Text:Fallback create layout',
413 'Text:Outside create layout',
414 'App create layout',
415 'Text:Inside:Before create passive',
416 'Text:Fallback create passive',
417 'Text:Outside create passive',
418 'App create passive',
419 ]);
420 expect(ReactNoop).toMatchRenderedOutput(
421 <>
422 <span prop="Inside:Before" hidden={true} />
423 <span prop="Inside:After" hidden={true} />
424 <span prop="Fallback" />
425 <span prop="Outside" />
426 </>,
427 );
428
429 // Resolving the suspended resource should
430 await act(async () => {
431 await resolveText('Async');
432 });
433 assertLog([
434 'AsyncText:Async render',
435 'Text:Fallback destroy insertion',
436 'Text:Fallback destroy layout',
437 'AsyncText:Async create layout',
438 'Text:Fallback destroy passive',
439 'AsyncText:Async create passive',
440 ]);
441 expect(ReactNoop).toMatchRenderedOutput(
442 <>
443 <span prop="Inside:Before" />
444 <span prop="Async" />
445 <span prop="Inside:After" />
446 <span prop="Outside" />
447 </>,
448 );
449
450 await act(() => {
451 ReactNoop.renderLegacySyncRoot(null);
452 });
453 assertLog([
454 'App destroy layout',
455 'Text:Inside:Before destroy insertion',
456 'Text:Inside:Before destroy layout',
457 'AsyncText:Async destroy layout',
458 'ClassText:Inside:After componentWillUnmount',
459 'Text:Outside destroy insertion',
460 'Text:Outside destroy layout',
461 'App destroy passive',
462 'Text:Inside:Before destroy passive',
463 'AsyncText:Async destroy passive',
464 'Text:Outside destroy passive',
465 ]);
466 expect(ReactNoop).toMatchRenderedOutput(null);
467 });
468 });
469
470 describe('effects within a tree that re-suspends in an update', () => {
471 // @gate enableLegacyCache && !disableLegacyMode
472 it('should not be destroyed or recreated in legacy roots', async () => {
473 function App({children = null}) {
474 Scheduler.log('App render');
475 React.useLayoutEffect(() => {
476 Scheduler.log('App create layout');
477 return () => {
478 Scheduler.log('App destroy layout');
479 };
480 }, []);
481 React.useEffect(() => {
482 Scheduler.log('App create passive');
483 return () => {
484 Scheduler.log('App destroy passive');
485 };
486 }, []);
487 return (
488 <>
489 <Suspense fallback={<Text text="Fallback" />}>
490 <Text text="Inside:Before" />
491 {children}
492 <Text text="Inside:After" />
493 </Suspense>
494 <Text text="Outside" />
495 </>
496 );
497 }
498
499 // Mount
500 await act(() => {
501 ReactNoop.renderLegacySyncRoot(<App />);
502 });
503 assertLog([
504 'App render',
505 'Text:Inside:Before render',
506 'Text:Inside:After render',
507 'Text:Outside render',
508 'Text:Inside:Before create insertion',
509 'Text:Inside:After create insertion',
510 'Text:Outside create insertion',
511 'Text:Inside:Before create layout',
512 'Text:Inside:After create layout',
513 'Text:Outside create layout',
514 'App create layout',
515 'Text:Inside:Before create passive',
516 'Text:Inside:After create passive',
517 'Text:Outside create passive',
518 'App create passive',
519 ]);
520 expect(ReactNoop).toMatchRenderedOutput(
521 <>
522 <span prop="Inside:Before" />
523 <span prop="Inside:After" />
524 <span prop="Outside" />
525 </>,
526 );
527
528 // Schedule an update that causes React to suspend.
529 await act(() => {
530 ReactNoop.renderLegacySyncRoot(
531 <App>
532 <AsyncText text="Async" />
533 </App>,
534 );
535 });
536 assertLog([
537 'App render',
538 'Text:Inside:Before render',
539 'Suspend:Async',
540 'Text:Inside:After render',
541 'Text:Fallback render',
542 'Text:Outside render',
543 'Text:Fallback create insertion',
544 'Text:Fallback create layout',
545 'Text:Fallback create passive',
546 ]);
547 expect(ReactNoop).toMatchRenderedOutput(
548 <>
549 <span prop="Inside:Before" hidden={true} />
550 <span prop="Inside:After" hidden={true} />
551 <span prop="Fallback" />
552 <span prop="Outside" />
553 </>,
554 );
555
556 await advanceTimers(1000);
557
558 // Noop since sync root has already committed
559 assertLog([]);
560 expect(ReactNoop).toMatchRenderedOutput(
561 <>
562 <span prop="Inside:Before" hidden={true} />
563 <span prop="Inside:After" hidden={true} />
564 <span prop="Fallback" />
565 <span prop="Outside" />
566 </>,
567 );
568
569 // Resolving the suspended resource should re-create inner layout effects.
570 await act(async () => {
571 await resolveText('Async');
572 });
573 assertLog([
574 'AsyncText:Async render',
575 'Text:Fallback destroy insertion',
576 'Text:Fallback destroy layout',
577 'AsyncText:Async create layout',
578 'Text:Fallback destroy passive',
579 'AsyncText:Async create passive',
580 ]);
581 expect(ReactNoop).toMatchRenderedOutput(
582 <>
583 <span prop="Inside:Before" />
584 <span prop="Async" />
585 <span prop="Inside:After" />
586 <span prop="Outside" />
587 </>,
588 );
589
590 await act(() => {
591 ReactNoop.renderLegacySyncRoot(null);
592 });
593 assertLog([
594 'App destroy layout',
595 'Text:Inside:Before destroy insertion',
596 'Text:Inside:Before destroy layout',
597 'AsyncText:Async destroy layout',
598 'Text:Inside:After destroy insertion',
599 'Text:Inside:After destroy layout',
600 'Text:Outside destroy insertion',
601 'Text:Outside destroy layout',
602 'App destroy passive',
603 'Text:Inside:Before destroy passive',
604 'AsyncText:Async destroy passive',
605 'Text:Inside:After destroy passive',
606 'Text:Outside destroy passive',
607 ]);
608 });
609
610 // @gate enableLegacyCache
611 it('should be destroyed and recreated for function components', async () => {
612 function App({children = null}) {
613 Scheduler.log('App render');
614 React.useLayoutEffect(() => {
615 Scheduler.log('App create layout');
616 return () => {
617 Scheduler.log('App destroy layout');
618 };
619 }, []);
620 React.useEffect(() => {
621 Scheduler.log('App create passive');
622 return () => {
623 Scheduler.log('App destroy passive');
624 };
625 }, []);
626 return (
627 <>
628 <Suspense fallback={<Text text="Fallback" />}>
629 <Text text="Inside:Before" />
630 {children}
631 <Text text="Inside:After" />
632 </Suspense>
633 <Text text="Outside" />
634 </>
635 );
636 }
637
638 await act(() => {
639 ReactNoop.render(<App />);
640 });
641 assertLog([
642 'App render',
643 'Text:Inside:Before render',
644 'Text:Inside:After render',
645 'Text:Outside render',
646 'Text:Inside:Before create insertion',
647 'Text:Inside:After create insertion',
648 'Text:Outside create insertion',
649 'Text:Inside:Before create layout',
650 'Text:Inside:After create layout',
651 'Text:Outside create layout',
652 'App create layout',
653 'Text:Inside:Before create passive',
654 'Text:Inside:After create passive',
655 'Text:Outside create passive',
656 'App create passive',
657 ]);
658 expect(ReactNoop).toMatchRenderedOutput(
659 <>
660 <span prop="Inside:Before" />
661 <span prop="Inside:After" />
662 <span prop="Outside" />
663 </>,
664 );
665
666 // Schedule an update that causes React to suspend.
667 await act(async () => {
668 ReactNoop.render(
669 <App>
670 <AsyncText text="Async" />
671 </App>,
672 );
673 await waitFor([
674 'App render',
675 'Text:Inside:Before render',
676 'Suspend:Async',
677 'Text:Fallback render',
678 'Text:Outside render',
679 'Text:Inside:Before destroy layout',
680 'Text:Inside:After destroy layout',
681 'Text:Fallback create insertion',
682 'Text:Fallback create layout',
683 ]);
684 await waitForAll([
685 'Text:Fallback create passive',
686 // pre-warming
687 'Text:Inside:Before render',
688 'Suspend:Async',
689 'Text:Inside:After render',
690 ]);
691 expect(ReactNoop).toMatchRenderedOutput(
692 <>
693 <span prop="Inside:Before" hidden={true} />
694 <span prop="Inside:After" hidden={true} />
695 <span prop="Fallback" />
696 <span prop="Outside" />
697 </>,
698 );
699 });
700
701 // Resolving the suspended resource should re-create inner layout effects.
702 await act(async () => {
703 await resolveText('Async');
704 });
705 assertLog([
706 'Text:Inside:Before render',
707 'AsyncText:Async render',
708 'Text:Inside:After render',
709 'Text:Fallback destroy insertion',
710 'Text:Fallback destroy layout',
711 'Text:Inside:Before create layout',
712 'AsyncText:Async create layout',
713 'Text:Inside:After create layout',
714 'Text:Fallback destroy passive',
715 'AsyncText:Async create passive',
716 ]);
717 expect(ReactNoop).toMatchRenderedOutput(
718 <>
719 <span prop="Inside:Before" />
720 <span prop="Async" />
721 <span prop="Inside:After" />
722 <span prop="Outside" />
723 </>,
724 );
725
726 await act(() => {
727 ReactNoop.render(null);
728 });
729 assertLog([
730 'App destroy layout',
731 'Text:Inside:Before destroy insertion',
732 'Text:Inside:Before destroy layout',
733 'AsyncText:Async destroy layout',
734 'Text:Inside:After destroy insertion',
735 'Text:Inside:After destroy layout',
736 'Text:Outside destroy insertion',
737 'Text:Outside destroy layout',
738 'App destroy passive',
739 'Text:Inside:Before destroy passive',
740 'AsyncText:Async destroy passive',
741 'Text:Inside:After destroy passive',
742 'Text:Outside destroy passive',
743 ]);
744 });
745
746 // @gate enableLegacyCache
747 it('should be destroyed and recreated for class components', async () => {
748 class ClassText extends React.Component {
749 componentDidMount() {
750 const {text} = this.props;
751 Scheduler.log(`ClassText:${text} componentDidMount`);
752 }
753 componentDidUpdate() {
754 const {text} = this.props;
755 Scheduler.log(`ClassText:${text} componentDidUpdate`);
756 }
757 componentWillUnmount() {
758 const {text} = this.props;
759 Scheduler.log(`ClassText:${text} componentWillUnmount`);
760 }
761 render() {
762 const {children, text} = this.props;
763 Scheduler.log(`ClassText:${text} render`);
764 return <span prop={text}>{children}</span>;
765 }
766 }
767
768 function App({children = null}) {
769 Scheduler.log('App render');
770 React.useLayoutEffect(() => {
771 Scheduler.log('App create layout');
772 return () => {
773 Scheduler.log('App destroy layout');
774 };
775 }, []);
776 React.useEffect(() => {
777 Scheduler.log('App create passive');
778 return () => {
779 Scheduler.log('App destroy passive');
780 };
781 }, []);
782 return (
783 <>
784 <Suspense fallback={<ClassText text="Fallback" />}>
785 <ClassText text="Inside:Before" />
786 {children}
787 <ClassText text="Inside:After" />
788 </Suspense>
789 <ClassText text="Outside" />
790 </>
791 );
792 }
793
794 // Mount
795 await act(() => {
796 ReactNoop.render(<App />);
797 });
798 assertLog([
799 'App render',
800 'ClassText:Inside:Before render',
801 'ClassText:Inside:After render',
802 'ClassText:Outside render',
803 'ClassText:Inside:Before componentDidMount',
804 'ClassText:Inside:After componentDidMount',
805 'ClassText:Outside componentDidMount',
806 'App create layout',
807 'App create passive',
808 ]);
809 expect(ReactNoop).toMatchRenderedOutput(
810 <>
811 <span prop="Inside:Before" />
812 <span prop="Inside:After" />
813 <span prop="Outside" />
814 </>,
815 );
816
817 // Schedule an update that causes React to suspend.
818 await act(async () => {
819 ReactNoop.render(
820 <App>
821 <AsyncText text="Async" />
822 </App>,
823 );
824
825 await waitFor([
826 'App render',
827 'ClassText:Inside:Before render',
828 'Suspend:Async',
829 'ClassText:Fallback render',
830 'ClassText:Outside render',
831 'ClassText:Inside:Before componentWillUnmount',
832 'ClassText:Inside:After componentWillUnmount',
833 'ClassText:Fallback componentDidMount',
834 'ClassText:Outside componentDidUpdate',
835 ]);
836 expect(ReactNoop).toMatchRenderedOutput(
837 <>
838 <span prop="Inside:Before" hidden={true} />
839 <span prop="Inside:After" hidden={true} />
840 <span prop="Fallback" />
841 <span prop="Outside" />
842 </>,
843 );
844 });
845 // pre-warming
846 assertLog([
847 'ClassText:Inside:Before render',
848 'Suspend:Async',
849 'ClassText:Inside:After render',
850 ]);
851
852 // Resolving the suspended resource should re-create inner layout effects.
853 await act(async () => {
854 await resolveText('Async');
855 });
856 assertLog([
857 'ClassText:Inside:Before render',
858 'AsyncText:Async render',
859 'ClassText:Inside:After render',
860 'ClassText:Fallback componentWillUnmount',
861 'ClassText:Inside:Before componentDidMount',
862 'AsyncText:Async create layout',
863 'ClassText:Inside:After componentDidMount',
864 'AsyncText:Async create passive',
865 ]);
866 expect(ReactNoop).toMatchRenderedOutput(
867 <>
868 <span prop="Inside:Before" />
869 <span prop="Async" />
870 <span prop="Inside:After" />
871 <span prop="Outside" />
872 </>,
873 );
874 await act(() => {
875 ReactNoop.render(null);
876 });
877 assertLog([
878 'App destroy layout',
879 'ClassText:Inside:Before componentWillUnmount',
880 'AsyncText:Async destroy layout',
881 'ClassText:Inside:After componentWillUnmount',
882 'ClassText:Outside componentWillUnmount',
883 'App destroy passive',
884 'AsyncText:Async destroy passive',
885 ]);
886 });
887
888 // @gate enableLegacyCache
889 it('should be destroyed and recreated when nested below host components', async () => {
890 function App({children = null}) {
891 Scheduler.log('App render');
892 React.useLayoutEffect(() => {
893 Scheduler.log('App create layout');
894 return () => {
895 Scheduler.log('App destroy layout');
896 };
897 }, []);
898 React.useEffect(() => {
899 Scheduler.log('App create passive');
900 return () => {
901 Scheduler.log('App destroy passive');
902 };
903 }, []);
904 return (
905 <Suspense fallback={<Text text="Fallback" />}>
906 {children}
907 <Text text="Outer">
908 <Text text="Inner" />
909 </Text>
910 </Suspense>
911 );
912 }
913
914 // Mount
915 await act(() => {
916 ReactNoop.render(<App />);
917 });
918 assertLog([
919 'App render',
920 'Text:Outer render',
921 'Text:Inner render',
922 'Text:Inner create insertion',
923 'Text:Outer create insertion',
924 'Text:Inner create layout',
925 'Text:Outer create layout',
926 'App create layout',
927 'Text:Inner create passive',
928 'Text:Outer create passive',
929 'App create passive',
930 ]);
931 expect(ReactNoop).toMatchRenderedOutput(
932 <span prop="Outer">
933 <span prop="Inner" />
934 </span>,
935 );
936
937 // Schedule an update that causes React to suspend.
938 await act(async () => {
939 ReactNoop.render(
940 <App>
941 <AsyncText text="Async" />
942 </App>,
943 );
944 await waitFor([
945 'App render',
946 'Suspend:Async',
947 'Text:Fallback render',
948 'Text:Outer destroy layout',
949 'Text:Inner destroy layout',
950 'Text:Fallback create insertion',
951 'Text:Fallback create layout',
952 ]);
953 await waitForAll([
954 'Text:Fallback create passive',
955 // pre-warming
956 'Suspend:Async',
957 'Text:Outer render',
958 'Text:Inner render',
959 ]);
960 expect(ReactNoop).toMatchRenderedOutput(
961 <>
962 <span hidden={true} prop="Outer">
963 <span prop="Inner" />
964 </span>
965 <span prop="Fallback" />
966 </>,
967 );
968 });
969
970 // Resolving the suspended resource should re-create inner layout effects.
971 await act(async () => {
972 await resolveText('Async');
973 });
974 assertLog([
975 'AsyncText:Async render',
976 'Text:Outer render',
977 'Text:Inner render',
978 'Text:Fallback destroy insertion',
979 'Text:Fallback destroy layout',
980 'AsyncText:Async create layout',
981 'Text:Inner create layout',
982 'Text:Outer create layout',
983 'Text:Fallback destroy passive',
984 'AsyncText:Async create passive',
985 ]);
986 expect(ReactNoop).toMatchRenderedOutput(
987 <>
988 <span prop="Async" />
989 <span prop="Outer">
990 <span prop="Inner" />
991 </span>
992 </>,
993 );
994
995 await act(() => {
996 ReactNoop.render(null);
997 });
998 assertLog([
999 'App destroy layout',
1000 'AsyncText:Async destroy layout',
1001 'Text:Outer destroy insertion',
1002 'Text:Outer destroy layout',
1003 'Text:Inner destroy insertion',
1004 'Text:Inner destroy layout',
1005 'App destroy passive',
1006 'AsyncText:Async destroy passive',
1007 'Text:Outer destroy passive',
1008 'Text:Inner destroy passive',
1009 ]);
1010 });
1011
1012 // @gate enableLegacyCache
1013 it('should be destroyed and recreated even if there is a bailout because of memoization', async () => {
1014 const MemoizedText = React.memo(Text, () => true);
1015
1016 function App({children = null}) {
1017 Scheduler.log('App render');
1018 React.useLayoutEffect(() => {
1019 Scheduler.log('App create layout');
1020 return () => {
1021 Scheduler.log('App destroy layout');
1022 };
1023 }, []);
1024 React.useEffect(() => {
1025 Scheduler.log('App create passive');
1026 return () => {
1027 Scheduler.log('App destroy passive');
1028 };
1029 }, []);
1030 return (
1031 <Suspense fallback={<Text text="Fallback" />}>
1032 {children}
1033 <Text text="Outer">
1034 <MemoizedText text="MemoizedInner" />
1035 </Text>
1036 </Suspense>
1037 );
1038 }
1039
1040 // Mount
1041 await act(() => {
1042 ReactNoop.render(<App />);
1043 });
1044 assertLog([
1045 'App render',
1046 'Text:Outer render',
1047 'Text:MemoizedInner render',
1048 'Text:MemoizedInner create insertion',
1049 'Text:Outer create insertion',
1050 'Text:MemoizedInner create layout',
1051 'Text:Outer create layout',
1052 'App create layout',
1053 'Text:MemoizedInner create passive',
1054 'Text:Outer create passive',
1055 'App create passive',
1056 ]);
1057 expect(ReactNoop).toMatchRenderedOutput(
1058 <span prop="Outer">
1059 <span prop="MemoizedInner" />
1060 </span>,
1061 );
1062
1063 // Schedule an update that causes React to suspend.
1064 await act(async () => {
1065 ReactNoop.render(
1066 <App>
1067 <AsyncText text="Async" />
1068 </App>,
1069 );
1070 await waitFor([
1071 'App render',
1072 'Suspend:Async',
1073 // Text:MemoizedInner is memoized
1074 'Text:Fallback render',
1075 'Text:Outer destroy layout',
1076 'Text:MemoizedInner destroy layout',
1077 'Text:Fallback create insertion',
1078 'Text:Fallback create layout',
1079 ]);
1080 await waitForAll([
1081 'Text:Fallback create passive',
1082 // pre-warming
1083 'Suspend:Async',
1084 'Text:Outer render',
1085 ]);
1086 expect(ReactNoop).toMatchRenderedOutput(
1087 <>
1088 <span hidden={true} prop="Outer">
1089 <span prop="MemoizedInner" />
1090 </span>
1091 <span prop="Fallback" />
1092 </>,
1093 );
1094 });
1095
1096 // Resolving the suspended resource should re-create inner layout effects.
1097 await act(async () => {
1098 await resolveText('Async');
1099 });
1100 assertLog([
1101 'AsyncText:Async render',
1102 'Text:Outer render',
1103 'Text:Fallback destroy insertion',
1104 'Text:Fallback destroy layout',
1105 'AsyncText:Async create layout',
1106 'Text:MemoizedInner create layout',
1107 'Text:Outer create layout',
1108 'Text:Fallback destroy passive',
1109 'AsyncText:Async create passive',
1110 ]);
1111 expect(ReactNoop).toMatchRenderedOutput(
1112 <>
1113 <span prop="Async" />
1114 <span prop="Outer">
1115 <span prop="MemoizedInner" />
1116 </span>
1117 </>,
1118 );
1119
1120 await act(() => {
1121 ReactNoop.render(null);
1122 });
1123 assertLog([
1124 'App destroy layout',
1125 'AsyncText:Async destroy layout',
1126 'Text:Outer destroy insertion',
1127 'Text:Outer destroy layout',
1128 'Text:MemoizedInner destroy insertion',
1129 'Text:MemoizedInner destroy layout',
1130 'App destroy passive',
1131 'AsyncText:Async destroy passive',
1132 'Text:Outer destroy passive',
1133 'Text:MemoizedInner destroy passive',
1134 ]);
1135 });
1136
1137 // @gate enableLegacyCache
1138 it('should respect nested suspense boundaries', async () => {
1139 function App({innerChildren = null, outerChildren = null}) {
1140 return (
1141 <Suspense fallback={<Text text="OuterFallback" />}>
1142 <Text text="Outer" />
1143 {outerChildren}
1144 <Suspense fallback={<Text text="InnerFallback" />}>
1145 <Text text="Inner" />
1146 {innerChildren}
1147 </Suspense>
1148 </Suspense>
1149 );
1150 }
1151
1152 // Mount
1153 await act(() => {
1154 ReactNoop.render(<App />);
1155 });
1156 assertLog([
1157 'Text:Outer render',
1158 'Text:Inner render',
1159 'Text:Outer create insertion',
1160 'Text:Inner create insertion',
1161 'Text:Outer create layout',
1162 'Text:Inner create layout',
1163 'Text:Outer create passive',
1164 'Text:Inner create passive',
1165 ]);
1166 expect(ReactNoop).toMatchRenderedOutput(
1167 <>
1168 <span prop="Outer" />
1169 <span prop="Inner" />
1170 </>,
1171 );
1172
1173 // Suspend the inner Suspense subtree (only inner effects should be destroyed)
1174 await act(() => {
1175 ReactNoop.render(
1176 <App innerChildren={<AsyncText text="InnerAsync_1" />} />,
1177 );
1178 });
1179 assertLog([
1180 'Text:Outer render',
1181 'Text:Inner render',
1182 'Suspend:InnerAsync_1',
1183 'Text:InnerFallback render',
1184 'Text:Inner destroy layout',
1185 'Text:InnerFallback create insertion',
1186 'Text:InnerFallback create layout',
1187 'Text:InnerFallback create passive',
1188 // pre-warming
1189 'Text:Inner render',
1190 'Suspend:InnerAsync_1',
1191 ]);
1192 expect(ReactNoop).toMatchRenderedOutput(
1193 <>
1194 <span prop="Outer" />
1195 <span prop="Inner" hidden={true} />
1196 <span prop="InnerFallback" />
1197 </>,
1198 );
1199
1200 // Suspend the outer Suspense subtree (outer effects and inner fallback effects should be destroyed)
1201 // (This check also ensures we don't destroy effects for mounted inner fallback.)
1202 await act(() => {
1203 ReactNoop.render(
1204 <App
1205 outerChildren={<AsyncText text="OuterAsync_1" />}
1206 innerChildren={<AsyncText text="InnerAsync_1" />}
1207 />,
1208 );
1209 });
1210 await advanceTimers(1000);
1211 assertLog([
1212 'Text:Outer render',
1213 'Suspend:OuterAsync_1',
1214 'Text:OuterFallback render',
1215 'Text:Outer destroy layout',
1216 'Text:InnerFallback destroy layout',
1217 'Text:OuterFallback create insertion',
1218 'Text:OuterFallback create layout',
1219 'Text:OuterFallback create passive',
1220 // pre-warming
1221 'Text:Outer render',
1222 'Suspend:OuterAsync_1',
1223 'Text:Inner render',
1224 'Suspend:InnerAsync_1',
1225 'Text:InnerFallback render',
1226 ]);
1227 expect(ReactNoop).toMatchRenderedOutput(
1228 <>
1229 <span prop="Outer" hidden={true} />
1230 <span prop="Inner" hidden={true} />
1231 <span prop="InnerFallback" hidden={true} />
1232 <span prop="OuterFallback" />
1233 </>,
1234 );
1235
1236 // Show the inner Suspense subtree (no effects should be recreated)
1237 await act(async () => {
1238 await resolveText('InnerAsync_1');
1239 });
1240 assertLog([
1241 'Text:Outer render',
1242 'Suspend:OuterAsync_1',
1243 // pre-warming
1244 'Text:Outer render',
1245 'Suspend:OuterAsync_1',
1246 'Text:Inner render',
1247 'AsyncText:InnerAsync_1 render',
1248 ]);
1249 expect(ReactNoop).toMatchRenderedOutput(
1250 <>
1251 <span prop="Outer" hidden={true} />
1252 <span prop="Inner" hidden={true} />
1253 <span prop="InnerFallback" hidden={true} />
1254 <span prop="OuterFallback" />
1255 </>,
1256 );
1257
1258 // Suspend the inner Suspense subtree (no effects should be destroyed)
1259 await act(() => {
1260 ReactNoop.render(
1261 <App
1262 outerChildren={<AsyncText text="OuterAsync_1" />}
1263 innerChildren={<AsyncText text="InnerAsync_2" />}
1264 />,
1265 );
1266 });
1267 await advanceTimers(1000);
1268 assertLog([
1269 'Text:Outer render',
1270 'Suspend:OuterAsync_1',
1271 'Text:OuterFallback render',
1272 // pre-warming
1273 'Text:Outer render',
1274 'Suspend:OuterAsync_1',
1275 'Text:Inner render',
1276 'Suspend:InnerAsync_2',
1277 'Text:InnerFallback render',
1278 ]);
1279 expect(ReactNoop).toMatchRenderedOutput(
1280 <>
1281 <span prop="Outer" hidden={true} />
1282 <span prop="Inner" hidden={true} />
1283 <span prop="InnerFallback" hidden={true} />
1284 <span prop="OuterFallback" />
1285 </>,
1286 );
1287
1288 // Show the outer Suspense subtree (only outer effects should be recreated)
1289 await act(async () => {
1290 await resolveText('OuterAsync_1');
1291 });
1292 assertLog([
1293 'Text:Outer render',
1294 'AsyncText:OuterAsync_1 render',
1295 'Text:Inner render',
1296 'Suspend:InnerAsync_2',
1297 'Text:InnerFallback render',
1298 'Text:OuterFallback destroy insertion',
1299 'Text:OuterFallback destroy layout',
1300 'Text:Outer create layout',
1301 'AsyncText:OuterAsync_1 create layout',
1302 'Text:InnerFallback create layout',
1303 'Text:OuterFallback destroy passive',
1304 'AsyncText:OuterAsync_1 create passive',
1305 // pre-warming
1306 'Text:Inner render',
1307 'Suspend:InnerAsync_2',
1308 ]);
1309 expect(ReactNoop).toMatchRenderedOutput(
1310 <>
1311 <span prop="Outer" />
1312 <span prop="OuterAsync_1" />
1313 <span prop="Inner" hidden={true} />
1314 <span prop="InnerFallback" />
1315 </>,
1316 );
1317
1318 // Show the inner Suspense subtree (only inner effects should be recreated)
1319 await act(async () => {
1320 await resolveText('InnerAsync_2');
1321 });
1322 assertLog([
1323 'Text:Inner render',
1324 'AsyncText:InnerAsync_2 render',
1325 'Text:InnerFallback destroy insertion',
1326 'Text:InnerFallback destroy layout',
1327 'Text:Inner create layout',
1328 'AsyncText:InnerAsync_2 create layout',
1329 'Text:InnerFallback destroy passive',
1330 'AsyncText:InnerAsync_2 create passive',
1331 ]);
1332 expect(ReactNoop).toMatchRenderedOutput(
1333 <>
1334 <span prop="Outer" />
1335 <span prop="OuterAsync_1" />
1336 <span prop="Inner" />
1337 <span prop="InnerAsync_2" />
1338 </>,
1339 );
1340
1341 // Suspend the outer Suspense subtree (all effects should be destroyed)
1342 await act(() => {
1343 ReactNoop.render(
1344 <App
1345 outerChildren={<AsyncText text="OuterAsync_2" />}
1346 innerChildren={<AsyncText text="InnerAsync_2" />}
1347 />,
1348 );
1349 });
1350 assertLog([
1351 'Text:Outer render',
1352 'Suspend:OuterAsync_2',
1353 'Text:OuterFallback render',
1354 'Text:Outer destroy layout',
1355 'AsyncText:OuterAsync_1 destroy layout',
1356 'Text:Inner destroy layout',
1357 'AsyncText:InnerAsync_2 destroy layout',
1358 'Text:OuterFallback create insertion',
1359 'Text:OuterFallback create layout',
1360 'Text:OuterFallback create passive',
1361 // pre-warming
1362 'Text:Outer render',
1363 'Suspend:OuterAsync_2',
1364 'Text:Inner render',
1365 'AsyncText:InnerAsync_2 render',
1366 ]);
1367 expect(ReactNoop).toMatchRenderedOutput(
1368 <>
1369 <span prop="Outer" hidden={true} />
1370 <span prop="OuterAsync_1" hidden={true} />
1371 <span prop="Inner" hidden={true} />
1372 <span prop="InnerAsync_2" hidden={true} />
1373 <span prop="OuterFallback" />
1374 </>,
1375 );
1376
1377 // Show the outer Suspense subtree (all effects should be recreated)
1378 await act(async () => {
1379 await resolveText('OuterAsync_2');
1380 });
1381 assertLog([
1382 'Text:Outer render',
1383 'AsyncText:OuterAsync_2 render',
1384 'Text:Inner render',
1385 'AsyncText:InnerAsync_2 render',
1386 'Text:OuterFallback destroy insertion',
1387 'Text:OuterFallback destroy layout',
1388 'Text:Outer create layout',
1389 'AsyncText:OuterAsync_2 create layout',
1390 'Text:Inner create layout',
1391 'AsyncText:InnerAsync_2 create layout',
1392 'Text:OuterFallback destroy passive',
1393 ]);
1394 expect(ReactNoop).toMatchRenderedOutput(
1395 <>
1396 <span prop="Outer" />
1397 <span prop="OuterAsync_2" />
1398 <span prop="Inner" />
1399 <span prop="InnerAsync_2" />
1400 </>,
1401 );
1402 });
1403
1404 // @gate enableLegacyCache
1405 it('should wait to reveal an inner child when inner one reveals first', async () => {
1406 function App({outerChildren, innerChildren}) {
1407 return (
1408 <Suspense fallback={<Text text="OuterFallback" />} name="Outer">
1409 <Suspense fallback={<Text text="InnerFallback" />} name="Inner">
1410 <div>{innerChildren}</div>
1411 </Suspense>
1412 {outerChildren}
1413 </Suspense>
1414 );
1415 }
1416
1417 // Mount
1418 await act(() => {
1419 ReactNoop.render(<App />);
1420 });
1421 assertLog([]);
1422 expect(ReactNoop).toMatchRenderedOutput(<div />);
1423
1424 // Resuspend inner boundary
1425 await act(() => {
1426 ReactNoop.render(
1427 <App
1428 outerChildren={null}
1429 innerChildren={<AsyncText text="InnerAsync" />}
1430 />,
1431 );
1432 });
1433 assertLog([
1434 'Suspend:InnerAsync',
1435 'Text:InnerFallback render',
1436 'Text:InnerFallback create insertion',
1437 'Text:InnerFallback create layout',
1438 'Text:InnerFallback create passive',
1439 'Suspend:InnerAsync',
1440 ]);
1441 expect(ReactNoop).toMatchRenderedOutput(
1442 <>
1443 <div hidden={true} />
1444 <span prop="InnerFallback" />
1445 </>,
1446 );
1447
1448 // Resuspend both boundaries
1449 await act(() => {
1450 ReactNoop.render(
1451 <App
1452 outerChildren={<AsyncText text="OuterAsync" />}
1453 innerChildren={<AsyncText text="InnerAsync" />}
1454 />,
1455 );
1456 });
1457 assertLog([
1458 'Suspend:InnerAsync',
1459 'Text:InnerFallback render',
1460 'Suspend:OuterAsync',
1461 'Text:OuterFallback render',
1462 'Text:InnerFallback destroy layout',
1463 'Text:OuterFallback create insertion',
1464 'Text:OuterFallback create layout',
1465 'Text:OuterFallback create passive',
1466 'Suspend:InnerAsync',
1467 'Text:InnerFallback render',
1468 'Suspend:OuterAsync',
1469 ]);
1470 expect(ReactNoop).toMatchRenderedOutput(
1471 <>
1472 <div hidden={true} />
1473 <span prop="InnerFallback" hidden={true} />
1474 <span prop="OuterFallback" />
1475 </>,
1476 );
1477
1478 // Unsuspend the inner Suspense subtree only
1479 // Interestingly, this never commits because the tree is left suspended.
1480 // If it did commit, it would potentially cause the div to incorrectly reappear.
1481 await act(() => {
1482 ReactNoop.render(
1483 <App
1484 outerChildren={<AsyncText text="OuterAsync" />}
1485 innerChildren={null}
1486 />,
1487 );
1488 });
1489 assertLog([
1490 'Suspend:OuterAsync',
1491 'Text:OuterFallback render',
1492 'Suspend:OuterAsync',
1493 ]);
1494 expect(ReactNoop).toMatchRenderedOutput(
1495 <>
1496 <div hidden={true} />
1497 <span prop="InnerFallback" hidden={true} />
1498 <span prop="OuterFallback" />
1499 </>,
1500 );
1501 });
1502
1503 // @gate enableLegacyCache
1504 it('should show nested host nodes if multiple boundaries resolve at the same time', async () => {
1505 function App({innerChildren = null, outerChildren = null}) {
1506 return (
1507 <Suspense fallback={<Text text="OuterFallback" />}>
1508 <Text text="Outer" />
1509 {outerChildren}
1510 <Suspense fallback={<Text text="InnerFallback" />}>
1511 <Text text="Inner" />
1512 {innerChildren}
1513 </Suspense>
1514 </Suspense>
1515 );
1516 }
1517
1518 // Mount
1519 await act(() => {
1520 ReactNoop.render(<App />);
1521 });
1522 assertLog([
1523 'Text:Outer render',
1524 'Text:Inner render',
1525 'Text:Outer create insertion',
1526 'Text:Inner create insertion',
1527 'Text:Outer create layout',
1528 'Text:Inner create layout',
1529 'Text:Outer create passive',
1530 'Text:Inner create passive',
1531 ]);
1532 expect(ReactNoop).toMatchRenderedOutput(
1533 <>
1534 <span prop="Outer" />
1535 <span prop="Inner" />
1536 </>,
1537 );
1538
1539 // Suspend the inner Suspense subtree (only inner effects should be destroyed)
1540 await act(() => {
1541 ReactNoop.render(
1542 <App innerChildren={<AsyncText text="InnerAsync_1" />} />,
1543 );
1544 });
1545 assertLog([
1546 'Text:Outer render',
1547 'Text:Inner render',
1548 'Suspend:InnerAsync_1',
1549 'Text:InnerFallback render',
1550 'Text:Inner destroy layout',
1551 'Text:InnerFallback create insertion',
1552 'Text:InnerFallback create layout',
1553 'Text:InnerFallback create passive',
1554 // pre-warming
1555 'Text:Inner render',
1556 'Suspend:InnerAsync_1',
1557 ]);
1558 expect(ReactNoop).toMatchRenderedOutput(
1559 <>
1560 <span prop="Outer" />
1561 <span prop="Inner" hidden={true} />
1562 <span prop="InnerFallback" />
1563 </>,
1564 );
1565
1566 // Suspend the outer Suspense subtree (outer effects and inner fallback effects should be destroyed)
1567 // (This check also ensures we don't destroy effects for mounted inner fallback.)
1568 await act(() => {
1569 ReactNoop.render(
1570 <App
1571 outerChildren={<AsyncText text="OuterAsync_1" />}
1572 innerChildren={<AsyncText text="InnerAsync_1" />}
1573 />,
1574 );
1575 });
1576 assertLog([
1577 'Text:Outer render',
1578 'Suspend:OuterAsync_1',
1579 'Text:OuterFallback render',
1580 'Text:Outer destroy layout',
1581 'Text:InnerFallback destroy layout',
1582 'Text:OuterFallback create insertion',
1583 'Text:OuterFallback create layout',
1584 'Text:OuterFallback create passive',
1585 // pre-warming
1586 'Text:Outer render',
1587 'Suspend:OuterAsync_1',
1588 'Text:Inner render',
1589 'Suspend:InnerAsync_1',
1590 'Text:InnerFallback render',
1591 ]);
1592 expect(ReactNoop).toMatchRenderedOutput(
1593 <>
1594 <span prop="Outer" hidden={true} />
1595 <span prop="Inner" hidden={true} />
1596 <span prop="InnerFallback" hidden={true} />
1597 <span prop="OuterFallback" />
1598 </>,
1599 );
1600
1601 // Resolve both suspended trees.
1602 await act(async () => {
1603 await resolveText('OuterAsync_1');
1604 await resolveText('InnerAsync_1');
1605 });
1606 assertLog([
1607 'Text:Outer render',
1608 'AsyncText:OuterAsync_1 render',
1609 'Text:Inner render',
1610 'AsyncText:InnerAsync_1 render',
1611 'Text:OuterFallback destroy insertion',
1612 'Text:OuterFallback destroy layout',
1613 'Text:InnerFallback destroy insertion',
1614 'Text:Outer create layout',
1615 'AsyncText:OuterAsync_1 create layout',
1616 'Text:Inner create layout',
1617 'AsyncText:InnerAsync_1 create layout',
1618 'Text:OuterFallback destroy passive',
1619 'Text:InnerFallback destroy passive',
1620 'AsyncText:OuterAsync_1 create passive',
1621 'AsyncText:InnerAsync_1 create passive',
1622 ]);
1623 expect(ReactNoop).toMatchRenderedOutput(
1624 <>
1625 <span prop="Outer" />
1626 <span prop="OuterAsync_1" />
1627 <span prop="Inner" />
1628 <span prop="InnerAsync_1" />
1629 </>,
1630 );
1631 });
1632
1633 // @gate enableLegacyCache
1634 it('should be cleaned up inside of a fallback that suspends', async () => {
1635 function App({fallbackChildren = null, outerChildren = null}) {
1636 return (
1637 <>
1638 <Suspense
1639 fallback={
1640 <>
1641 <Suspense fallback={<Text text="Fallback:Fallback" />}>
1642 <Text text="Fallback:Inside" />
1643 {fallbackChildren}
1644 </Suspense>
1645 <Text text="Fallback:Outside" />
1646 </>
1647 }>
1648 <Text text="Inside" />
1649 {outerChildren}
1650 </Suspense>
1651 <Text text="Outside" />
1652 </>
1653 );
1654 }
1655
1656 // Mount
1657 await act(() => {
1658 ReactNoop.render(<App />);
1659 });
1660 assertLog([
1661 'Text:Inside render',
1662 'Text:Outside render',
1663 'Text:Inside create insertion',
1664 'Text:Outside create insertion',
1665 'Text:Inside create layout',
1666 'Text:Outside create layout',
1667 'Text:Inside create passive',
1668 'Text:Outside create passive',
1669 ]);
1670 expect(ReactNoop).toMatchRenderedOutput(
1671 <>
1672 <span prop="Inside" />
1673 <span prop="Outside" />
1674 </>,
1675 );
1676
1677 // Suspend the outer shell
1678 await act(async () => {
1679 ReactNoop.render(
1680 <App outerChildren={<AsyncText text="OutsideAsync" />} />,
1681 );
1682 await waitFor([
1683 'Text:Inside render',
1684 'Suspend:OutsideAsync',
1685 'Text:Fallback:Inside render',
1686 'Text:Fallback:Outside render',
1687 'Text:Outside render',
1688 'Text:Inside destroy layout',
1689 'Text:Fallback:Inside create insertion',
1690 'Text:Fallback:Outside create insertion',
1691 'Text:Fallback:Inside create layout',
1692 'Text:Fallback:Outside create layout',
1693 ]);
1694 await waitForAll([
1695 'Text:Fallback:Inside create passive',
1696 'Text:Fallback:Outside create passive',
1697 // pre-warming
1698 'Text:Inside render',
1699 'Suspend:OutsideAsync',
1700 ]);
1701 expect(ReactNoop).toMatchRenderedOutput(
1702 <>
1703 <span prop="Inside" hidden={true} />
1704 <span prop="Fallback:Inside" />
1705 <span prop="Fallback:Outside" />
1706 <span prop="Outside" />
1707 </>,
1708 );
1709 });
1710
1711 // Suspend the fallback and verify that it's effects get cleaned up as well
1712 await act(async () => {
1713 ReactNoop.render(
1714 <App
1715 fallbackChildren={<AsyncText text="FallbackAsync" />}
1716 outerChildren={<AsyncText text="OutsideAsync" />}
1717 />,
1718 );
1719 await waitFor([
1720 'Text:Inside render',
1721 'Suspend:OutsideAsync',
1722 'Text:Fallback:Inside render',
1723 'Suspend:FallbackAsync',
1724 'Text:Fallback:Fallback render',
1725 'Text:Fallback:Outside render',
1726 'Text:Outside render',
1727 'Text:Fallback:Inside destroy layout',
1728 'Text:Fallback:Fallback create insertion',
1729 'Text:Fallback:Fallback create layout',
1730 ]);
1731 await waitForAll([
1732 'Text:Fallback:Fallback create passive',
1733 // pre-warming
1734 'Text:Inside render',
1735 'Suspend:OutsideAsync',
1736 'Text:Fallback:Inside render',
1737 'Suspend:FallbackAsync',
1738 ]);
1739 expect(ReactNoop).toMatchRenderedOutput(
1740 <>
1741 <span prop="Inside" hidden={true} />
1742 <span prop="Fallback:Inside" hidden={true} />
1743 <span prop="Fallback:Fallback" />
1744 <span prop="Fallback:Outside" />
1745 <span prop="Outside" />
1746 </>,
1747 );
1748 });
1749
1750 // Resolving both resources should cleanup fallback effects and recreate main effects
1751 await act(async () => {
1752 await resolveText('FallbackAsync');
1753 await resolveText('OutsideAsync');
1754 });
1755 assertLog([
1756 'Text:Inside render',
1757 'AsyncText:OutsideAsync render',
1758 'Text:Fallback:Inside destroy insertion',
1759 'Text:Fallback:Fallback destroy insertion',
1760 'Text:Fallback:Fallback destroy layout',
1761 'Text:Fallback:Outside destroy insertion',
1762 'Text:Fallback:Outside destroy layout',
1763 'Text:Inside create layout',
1764 'AsyncText:OutsideAsync create layout',
1765 'Text:Fallback:Inside destroy passive',
1766 'Text:Fallback:Fallback destroy passive',
1767 'Text:Fallback:Outside destroy passive',
1768 'AsyncText:OutsideAsync create passive',
1769 ]);
1770 expect(ReactNoop).toMatchRenderedOutput(
1771 <>
1772 <span prop="Inside" />
1773 <span prop="OutsideAsync" />
1774 <span prop="Outside" />
1775 </>,
1776 );
1777 });
1778
1779 // @gate enableLegacyCache
1780 it('should be cleaned up inside of a fallback that suspends (alternate)', async () => {
1781 function App({fallbackChildren = null, outerChildren = null}) {
1782 return (
1783 <>
1784 <Suspense
1785 fallback={
1786 <>
1787 <Suspense fallback={<Text text="Fallback:Fallback" />}>
1788 <Text text="Fallback:Inside" />
1789 {fallbackChildren}
1790 </Suspense>
1791 <Text text="Fallback:Outside" />
1792 </>
1793 }>
1794 <Text text="Inside" />
1795 {outerChildren}
1796 </Suspense>
1797 <Text text="Outside" />
1798 </>
1799 );
1800 }
1801
1802 // Mount
1803 await act(() => {
1804 ReactNoop.render(<App />);
1805 });
1806 assertLog([
1807 'Text:Inside render',
1808 'Text:Outside render',
1809 'Text:Inside create insertion',
1810 'Text:Outside create insertion',
1811 'Text:Inside create layout',
1812 'Text:Outside create layout',
1813 'Text:Inside create passive',
1814 'Text:Outside create passive',
1815 ]);
1816 expect(ReactNoop).toMatchRenderedOutput(
1817 <>
1818 <span prop="Inside" />
1819 <span prop="Outside" />
1820 </>,
1821 );
1822
1823 // Suspend both the outer boundary and the fallback
1824 await act(() => {
1825 ReactNoop.render(
1826 <App
1827 outerChildren={<AsyncText text="OutsideAsync" />}
1828 fallbackChildren={<AsyncText text="FallbackAsync" />}
1829 />,
1830 );
1831 });
1832 assertLog([
1833 'Text:Inside render',
1834 'Suspend:OutsideAsync',
1835 'Text:Fallback:Inside render',
1836 'Suspend:FallbackAsync',
1837 'Text:Fallback:Fallback render',
1838 'Text:Fallback:Outside render',
1839 'Text:Outside render',
1840 'Text:Inside destroy layout',
1841 'Text:Fallback:Fallback create insertion',
1842 'Text:Fallback:Outside create insertion',
1843 'Text:Fallback:Fallback create layout',
1844 'Text:Fallback:Outside create layout',
1845 'Text:Fallback:Fallback create passive',
1846 'Text:Fallback:Outside create passive',
1847 // pre-warming
1848 'Text:Inside render',
1849 'Suspend:OutsideAsync',
1850 'Text:Fallback:Inside render',
1851 'Suspend:FallbackAsync',
1852 ]);
1853 expect(ReactNoop).toMatchRenderedOutput(
1854 <>
1855 <span prop="Inside" hidden={true} />
1856 <span prop="Fallback:Fallback" />
1857 <span prop="Fallback:Outside" />
1858 <span prop="Outside" />
1859 </>,
1860 );
1861
1862 // Resolving the inside fallback
1863 await act(async () => {
1864 await resolveText('FallbackAsync');
1865 });
1866 assertLog([
1867 'Text:Fallback:Inside render',
1868 'AsyncText:FallbackAsync render',
1869 'Text:Fallback:Fallback destroy insertion',
1870 'Text:Fallback:Fallback destroy layout',
1871 'Text:Fallback:Inside create insertion',
1872 'Text:Fallback:Inside create layout',
1873 'AsyncText:FallbackAsync create layout',
1874 'Text:Fallback:Fallback destroy passive',
1875 'Text:Fallback:Inside create passive',
1876 'AsyncText:FallbackAsync create passive',
1877 ]);
1878 expect(ReactNoop).toMatchRenderedOutput(
1879 <>
1880 <span prop="Inside" hidden={true} />
1881 <span prop="Fallback:Inside" />
1882 <span prop="FallbackAsync" />
1883 <span prop="Fallback:Outside" />
1884 <span prop="Outside" />
1885 </>,
1886 );
1887
1888 // Resolving the outer fallback only
1889 await act(async () => {
1890 await resolveText('OutsideAsync');
1891 });
1892 assertLog([
1893 'Text:Inside render',
1894 'AsyncText:OutsideAsync render',
1895 'Text:Fallback:Inside destroy insertion',
1896 'Text:Fallback:Inside destroy layout',
1897 'AsyncText:FallbackAsync destroy layout',
1898 'Text:Fallback:Outside destroy insertion',
1899 'Text:Fallback:Outside destroy layout',
1900 'Text:Inside create layout',
1901 'AsyncText:OutsideAsync create layout',
1902 'Text:Fallback:Inside destroy passive',
1903 'AsyncText:FallbackAsync destroy passive',
1904 'Text:Fallback:Outside destroy passive',
1905 'AsyncText:OutsideAsync create passive',
1906 ]);
1907 expect(ReactNoop).toMatchRenderedOutput(
1908 <>
1909 <span prop="Inside" />
1910 <span prop="OutsideAsync" />
1911 <span prop="Outside" />
1912 </>,
1913 );
1914 });
1915
1916 // @gate enableLegacyCache
1917 it('should be cleaned up deeper inside of a subtree that suspends', async () => {
1918 function ConditionalSuspense({shouldSuspend}) {
1919 if (shouldSuspend) {
1920 readText('Suspend');
1921 }
1922 return <Text text="Inside" />;
1923 }
1924
1925 function App({children = null, shouldSuspend}) {
1926 return (
1927 <>
1928 <Suspense fallback={<Text text="Fallback" />}>
1929 <ConditionalSuspense shouldSuspend={shouldSuspend} />
1930 </Suspense>
1931 <Text text="Outside" />
1932 </>
1933 );
1934 }
1935
1936 // Mount
1937 await act(() => {
1938 ReactNoop.render(<App shouldSuspend={false} />);
1939 });
1940 assertLog([
1941 'Text:Inside render',
1942 'Text:Outside render',
1943 'Text:Inside create insertion',
1944 'Text:Outside create insertion',
1945 'Text:Inside create layout',
1946 'Text:Outside create layout',
1947 'Text:Inside create passive',
1948 'Text:Outside create passive',
1949 ]);
1950 expect(ReactNoop).toMatchRenderedOutput(
1951 <>
1952 <span prop="Inside" />
1953 <span prop="Outside" />
1954 </>,
1955 );
1956
1957 // Suspending a component in the middle of the tree
1958 // should still properly cleanup effects deeper in the tree
1959 await act(async () => {
1960 ReactNoop.render(<App shouldSuspend={true} />);
1961 await waitFor([
1962 'Suspend:Suspend',
1963 'Text:Fallback render',
1964 'Text:Outside render',
1965 'Text:Inside destroy layout',
1966 'Text:Fallback create insertion',
1967 'Text:Fallback create layout',
1968 ]);
1969 await waitForAll([
1970 'Text:Fallback create passive',
1971 // pre-warming
1972 'Suspend:Suspend',
1973 ]);
1974 expect(ReactNoop).toMatchRenderedOutput(
1975 <>
1976 <span prop="Inside" hidden={true} />
1977 <span prop="Fallback" />
1978 <span prop="Outside" />
1979 </>,
1980 );
1981 });
1982
1983 // Resolving should cleanup.
1984 await act(async () => {
1985 await resolveText('Suspend');
1986 });
1987 assertLog([
1988 'Text:Inside render',
1989 'Text:Fallback destroy insertion',
1990 'Text:Fallback destroy layout',
1991 'Text:Inside create layout',
1992 'Text:Fallback destroy passive',
1993 ]);
1994 expect(ReactNoop).toMatchRenderedOutput(
1995 <>
1996 <span prop="Inside" />
1997 <span prop="Outside" />
1998 </>,
1999 );
2000 });
2001
2002 describe('that throw errors', () => {
2003 // @gate enableLegacyCache
2004 it('are properly handled for componentDidMount', async () => {
2005 let componentDidMountShouldThrow = false;
2006
2007 class ThrowsInDidMount extends React.Component {
2008 componentWillUnmount() {
2009 Scheduler.log('ThrowsInDidMount componentWillUnmount');
2010 }
2011 componentDidMount() {
2012 Scheduler.log('ThrowsInDidMount componentDidMount');
2013 if (componentDidMountShouldThrow) {
2014 throw Error('expected');
2015 }
2016 }
2017 render() {
2018 Scheduler.log('ThrowsInDidMount render');
2019 return <span prop="ThrowsInDidMount" />;
2020 }
2021 }
2022
2023 function App({children = null}) {
2024 Scheduler.log('App render');
2025 React.useLayoutEffect(() => {
2026 Scheduler.log('App create layout');
2027 return () => {
2028 Scheduler.log('App destroy layout');
2029 };
2030 }, []);
2031 return (
2032 <>
2033 <Suspense fallback={<Text text="Fallback" />}>
2034 {children}
2035 <ThrowsInDidMount />
2036 <Text text="Inside" />
2037 </Suspense>
2038 <Text text="Outside" />
2039 </>
2040 );
2041 }
2042
2043 await act(() => {
2044 ReactNoop.render(
2045 <ErrorBoundary fallback={<Text text="Error" />}>
2046 <App />
2047 </ErrorBoundary>,
2048 );
2049 });
2050 assertLog([
2051 'ErrorBoundary render: try',
2052 'App render',
2053 'ThrowsInDidMount render',
2054 'Text:Inside render',
2055 'Text:Outside render',
2056 'Text:Inside create insertion',
2057 'Text:Outside create insertion',
2058 'ThrowsInDidMount componentDidMount',
2059 'Text:Inside create layout',
2060 'Text:Outside create layout',
2061 'App create layout',
2062 'Text:Inside create passive',
2063 'Text:Outside create passive',
2064 ]);
2065 expect(ReactNoop).toMatchRenderedOutput(
2066 <>
2067 <span prop="ThrowsInDidMount" />
2068 <span prop="Inside" />
2069 <span prop="Outside" />
2070 </>,
2071 );
2072
2073 // Schedule an update that causes React to suspend.
2074 await act(() => {
2075 ReactNoop.render(
2076 <ErrorBoundary fallback={<Text text="Error" />}>
2077 <App>
2078 <AsyncText text="Async" />
2079 </App>
2080 </ErrorBoundary>,
2081 );
2082 });
2083 assertLog([
2084 'ErrorBoundary render: try',
2085 'App render',
2086 'Suspend:Async',
2087 'Text:Fallback render',
2088 'Text:Outside render',
2089 'ThrowsInDidMount componentWillUnmount',
2090 'Text:Inside destroy layout',
2091 'Text:Fallback create insertion',
2092 'Text:Fallback create layout',
2093 'Text:Fallback create passive',
2094 // pre-warming
2095 'Suspend:Async',
2096 'ThrowsInDidMount render',
2097 'Text:Inside render',
2098 ]);
2099 expect(ReactNoop).toMatchRenderedOutput(
2100 <>
2101 <span prop="ThrowsInDidMount" hidden={true} />
2102 <span prop="Inside" hidden={true} />
2103 <span prop="Fallback" />
2104 <span prop="Outside" />
2105 </>,
2106 );
2107
2108 // Resolve the pending suspense and throw
2109 componentDidMountShouldThrow = true;
2110 await act(async () => {
2111 await resolveText('Async');
2112 });
2113 assertLog([
2114 'AsyncText:Async render',
2115 'ThrowsInDidMount render',
2116 'Text:Inside render',
2117 'Text:Fallback destroy insertion',
2118 'Text:Fallback destroy layout',
2119 'AsyncText:Async create layout',
2120
2121 // Even though an error was thrown in componentDidMount,
2122 // subsequent layout effects should still be destroyed.
2123 'ThrowsInDidMount componentDidMount',
2124 'Text:Inside create layout',
2125
2126 // Finish the in-progress commit
2127 'Text:Fallback destroy passive',
2128 'AsyncText:Async create passive',
2129
2130 // Destroy insertion, layout, and passive effects in the errored tree.
2131 'App destroy layout',
2132 'AsyncText:Async destroy layout',
2133 'ThrowsInDidMount componentWillUnmount',
2134 'Text:Inside destroy insertion',
2135 'Text:Inside destroy layout',
2136 'Text:Outside destroy insertion',
2137 'Text:Outside destroy layout',
2138 'AsyncText:Async destroy passive',
2139 'Text:Inside destroy passive',
2140 'Text:Outside destroy passive',
2141
2142 // Render fallback
2143 'ErrorBoundary render: catch',
2144 'Text:Error render',
2145 'Text:Error create insertion',
2146 'Text:Error create layout',
2147 'Text:Error create passive',
2148 ]);
2149 expect(ReactNoop).toMatchRenderedOutput(<span prop="Error" />);
2150 });
2151
2152 // @gate enableLegacyCache
2153 it('are properly handled for componentWillUnmount', async () => {
2154 class ThrowsInWillUnmount extends React.Component {
2155 componentDidMount() {
2156 Scheduler.log('ThrowsInWillUnmount componentDidMount');
2157 }
2158 componentWillUnmount() {
2159 Scheduler.log('ThrowsInWillUnmount componentWillUnmount');
2160 throw Error('expected');
2161 }
2162 render() {
2163 Scheduler.log('ThrowsInWillUnmount render');
2164 return <span prop="ThrowsInWillUnmount" />;
2165 }
2166 }
2167
2168 function App({children = null}) {
2169 Scheduler.log('App render');
2170 React.useLayoutEffect(() => {
2171 Scheduler.log('App create layout');
2172 return () => {
2173 Scheduler.log('App destroy layout');
2174 };
2175 }, []);
2176 return (
2177 <>
2178 <Suspense fallback={<Text text="Fallback" />}>
2179 {children}
2180 <ThrowsInWillUnmount />
2181 <Text text="Inside" />
2182 </Suspense>
2183 <Text text="Outside" />
2184 </>
2185 );
2186 }
2187
2188 await act(() => {
2189 ReactNoop.render(
2190 <ErrorBoundary fallback={<Text text="Error" />}>
2191 <App />
2192 </ErrorBoundary>,
2193 );
2194 });
2195 assertLog([
2196 'ErrorBoundary render: try',
2197 'App render',
2198 'ThrowsInWillUnmount render',
2199 'Text:Inside render',
2200 'Text:Outside render',
2201 'Text:Inside create insertion',
2202 'Text:Outside create insertion',
2203 'ThrowsInWillUnmount componentDidMount',
2204 'Text:Inside create layout',
2205 'Text:Outside create layout',
2206 'App create layout',
2207 'Text:Inside create passive',
2208 'Text:Outside create passive',
2209 ]);
2210 expect(ReactNoop).toMatchRenderedOutput(
2211 <>
2212 <span prop="ThrowsInWillUnmount" />
2213 <span prop="Inside" />
2214 <span prop="Outside" />
2215 </>,
2216 );
2217
2218 // Schedule an update that suspends and triggers our error code.
2219 await act(() => {
2220 ReactNoop.render(
2221 <ErrorBoundary fallback={<Text text="Error" />}>
2222 <App>
2223 <AsyncText text="Async" />
2224 </App>
2225 </ErrorBoundary>,
2226 );
2227 });
2228 assertLog([
2229 'ErrorBoundary render: try',
2230 'App render',
2231 'Suspend:Async',
2232 'Text:Fallback render',
2233 'Text:Outside render',
2234
2235 // Even though an error was thrown in componentWillUnmount,
2236 // subsequent layout effects should still be destroyed.
2237 'ThrowsInWillUnmount componentWillUnmount',
2238 'Text:Inside destroy layout',
2239
2240 // Finish the in-progress commit
2241 'Text:Fallback create insertion',
2242 'Text:Fallback create layout',
2243 'Text:Fallback create passive',
2244
2245 // Destroy layout and passive effects in the errored tree.
2246 'App destroy layout',
2247 'Text:Inside destroy insertion',
2248 'Text:Fallback destroy insertion',
2249 'Text:Fallback destroy layout',
2250 'Text:Outside destroy insertion',
2251 'Text:Outside destroy layout',
2252 'Text:Inside destroy passive',
2253 'Text:Fallback destroy passive',
2254 'Text:Outside destroy passive',
2255
2256 // Render fallback
2257 'ErrorBoundary render: catch',
2258 'Text:Error render',
2259 'Text:Error create insertion',
2260 'Text:Error create layout',
2261 'Text:Error create passive',
2262 ]);
2263 expect(ReactNoop).toMatchRenderedOutput(<span prop="Error" />);
2264 });
2265
2266 // @gate enableLegacyCache
2267 it('are properly handled for layout effect creation', async () => {
2268 let useLayoutEffectShouldThrow = false;
2269
2270 function ThrowsInLayoutEffect({unused}) {
2271 Scheduler.log('ThrowsInLayoutEffect render');
2272 React.useLayoutEffect(() => {
2273 Scheduler.log('ThrowsInLayoutEffect useLayoutEffect create');
2274 if (useLayoutEffectShouldThrow) {
2275 throw Error('expected');
2276 }
2277 return () => {
2278 Scheduler.log('ThrowsInLayoutEffect useLayoutEffect destroy');
2279 };
2280 }, []);
2281 return <span prop="ThrowsInLayoutEffect" />;
2282 }
2283
2284 function App({children = null}) {
2285 Scheduler.log('App render');
2286 React.useLayoutEffect(() => {
2287 Scheduler.log('App create layout');
2288 return () => {
2289 Scheduler.log('App destroy layout');
2290 };
2291 }, []);
2292 return (
2293 <>
2294 <Suspense fallback={<Text text="Fallback" />}>
2295 {children}
2296 <ThrowsInLayoutEffect />
2297 <Text text="Inside" />
2298 </Suspense>
2299 <Text text="Outside" />
2300 </>
2301 );
2302 }
2303
2304 await act(() => {
2305 ReactNoop.render(
2306 <ErrorBoundary fallback={<Text text="Error" />}>
2307 <App />
2308 </ErrorBoundary>,
2309 );
2310 });
2311 assertLog([
2312 'ErrorBoundary render: try',
2313 'App render',
2314 'ThrowsInLayoutEffect render',
2315 'Text:Inside render',
2316 'Text:Outside render',
2317 'Text:Inside create insertion',
2318 'Text:Outside create insertion',
2319 'ThrowsInLayoutEffect useLayoutEffect create',
2320 'Text:Inside create layout',
2321 'Text:Outside create layout',
2322 'App create layout',
2323 'Text:Inside create passive',
2324 'Text:Outside create passive',
2325 ]);
2326 expect(ReactNoop).toMatchRenderedOutput(
2327 <>
2328 <span prop="ThrowsInLayoutEffect" />
2329 <span prop="Inside" />
2330 <span prop="Outside" />
2331 </>,
2332 );
2333
2334 // Schedule an update that causes React to suspend.
2335 await act(() => {
2336 ReactNoop.render(
2337 <ErrorBoundary fallback={<Text text="Error" />}>
2338 <App>
2339 <AsyncText text="Async" />
2340 </App>
2341 </ErrorBoundary>,
2342 );
2343 });
2344 assertLog([
2345 'ErrorBoundary render: try',
2346 'App render',
2347 'Suspend:Async',
2348 'Text:Fallback render',
2349 'Text:Outside render',
2350 'ThrowsInLayoutEffect useLayoutEffect destroy',
2351 'Text:Inside destroy layout',
2352 'Text:Fallback create insertion',
2353 'Text:Fallback create layout',
2354 'Text:Fallback create passive',
2355 // pre-warming
2356 'Suspend:Async',
2357 'ThrowsInLayoutEffect render',
2358 'Text:Inside render',
2359 ]);
2360 expect(ReactNoop).toMatchRenderedOutput(
2361 <>
2362 <span prop="ThrowsInLayoutEffect" hidden={true} />
2363 <span prop="Inside" hidden={true} />
2364 <span prop="Fallback" />
2365 <span prop="Outside" />
2366 </>,
2367 );
2368
2369 // Resolve the pending suspense and throw
2370 useLayoutEffectShouldThrow = true;
2371 await act(async () => {
2372 await resolveText('Async');
2373 });
2374 assertLog([
2375 'AsyncText:Async render',
2376 'ThrowsInLayoutEffect render',
2377 'Text:Inside render',
2378
2379 'Text:Fallback destroy insertion',
2380 'Text:Fallback destroy layout',
2381
2382 // Even though an error was thrown in useLayoutEffect,
2383 // subsequent layout effects should still be created.
2384 'AsyncText:Async create layout',
2385 'ThrowsInLayoutEffect useLayoutEffect create',
2386 'Text:Inside create layout',
2387
2388 // Finish the in-progress commit
2389 'Text:Fallback destroy passive',
2390 'AsyncText:Async create passive',
2391
2392 // Destroy layout and passive effects in the errored tree.
2393 'App destroy layout',
2394 'AsyncText:Async destroy layout',
2395 'Text:Inside destroy insertion',
2396 'Text:Inside destroy layout',
2397 'Text:Outside destroy insertion',
2398 'Text:Outside destroy layout',
2399 'AsyncText:Async destroy passive',
2400 'Text:Inside destroy passive',
2401 'Text:Outside destroy passive',
2402
2403 // Render fallback
2404 'ErrorBoundary render: catch',
2405 'Text:Error render',
2406 'Text:Error create insertion',
2407 'Text:Error create layout',
2408 'Text:Error create passive',
2409 ]);
2410 expect(ReactNoop).toMatchRenderedOutput(<span prop="Error" />);
2411 });
2412
2413 // @gate enableLegacyCache
2414 it('are properly handled for layout effect destruction', async () => {
2415 function ThrowsInLayoutEffectDestroy({unused}) {
2416 Scheduler.log('ThrowsInLayoutEffectDestroy render');
2417 React.useLayoutEffect(() => {
2418 Scheduler.log('ThrowsInLayoutEffectDestroy useLayoutEffect create');
2419 return () => {
2420 Scheduler.log(
2421 'ThrowsInLayoutEffectDestroy useLayoutEffect destroy',
2422 );
2423 throw Error('expected');
2424 };
2425 }, []);
2426 return <span prop="ThrowsInLayoutEffectDestroy" />;
2427 }
2428
2429 function App({children = null}) {
2430 Scheduler.log('App render');
2431 React.useLayoutEffect(() => {
2432 Scheduler.log('App create layout');
2433 return () => {
2434 Scheduler.log('App destroy layout');
2435 };
2436 }, []);
2437 return (
2438 <>
2439 <Suspense fallback={<Text text="Fallback" />}>
2440 {children}
2441 <ThrowsInLayoutEffectDestroy />
2442 <Text text="Inside" />
2443 </Suspense>
2444 <Text text="Outside" />
2445 </>
2446 );
2447 }
2448
2449 await act(() => {
2450 ReactNoop.render(
2451 <ErrorBoundary fallback={<Text text="Error" />}>
2452 <App />
2453 </ErrorBoundary>,
2454 );
2455 });
2456 assertLog([
2457 'ErrorBoundary render: try',
2458 'App render',
2459 'ThrowsInLayoutEffectDestroy render',
2460 'Text:Inside render',
2461 'Text:Outside render',
2462 'Text:Inside create insertion',
2463 'Text:Outside create insertion',
2464 'ThrowsInLayoutEffectDestroy useLayoutEffect create',
2465 'Text:Inside create layout',
2466 'Text:Outside create layout',
2467 'App create layout',
2468 'Text:Inside create passive',
2469 'Text:Outside create passive',
2470 ]);
2471 expect(ReactNoop).toMatchRenderedOutput(
2472 <>
2473 <span prop="ThrowsInLayoutEffectDestroy" />
2474 <span prop="Inside" />
2475 <span prop="Outside" />
2476 </>,
2477 );
2478
2479 // Schedule an update that suspends and triggers our error code.
2480 await act(() => {
2481 ReactNoop.render(
2482 <ErrorBoundary fallback={<Text text="Error" />}>
2483 <App>
2484 <AsyncText text="Async" />
2485 </App>
2486 </ErrorBoundary>,
2487 );
2488 });
2489 assertLog([
2490 'ErrorBoundary render: try',
2491 'App render',
2492 'Suspend:Async',
2493 'Text:Fallback render',
2494 'Text:Outside render',
2495
2496 // Even though an error was thrown in useLayoutEffect destroy,
2497 // subsequent layout effects should still be destroyed.
2498 'ThrowsInLayoutEffectDestroy useLayoutEffect destroy',
2499 'Text:Inside destroy layout',
2500
2501 // Finish the in-progress commit
2502 'Text:Fallback create insertion',
2503 'Text:Fallback create layout',
2504 'Text:Fallback create passive',
2505
2506 // Destroy layout and passive effects in the errored tree.
2507 'App destroy layout',
2508 'Text:Inside destroy insertion',
2509 'Text:Fallback destroy insertion',
2510 'Text:Fallback destroy layout',
2511 'Text:Outside destroy insertion',
2512 'Text:Outside destroy layout',
2513 'Text:Inside destroy passive',
2514 'Text:Fallback destroy passive',
2515 'Text:Outside destroy passive',
2516
2517 // Render fallback
2518 'ErrorBoundary render: catch',
2519 'Text:Error render',
2520 'Text:Error create insertion',
2521 'Text:Error create layout',
2522 'Text:Error create passive',
2523 ]);
2524 expect(ReactNoop).toMatchRenderedOutput(<span prop="Error" />);
2525 });
2526 });
2527
2528 // @gate enableLegacyCache
2529 it('should be only destroy layout effects once if a tree suspends in multiple places', async () => {
2530 class ClassText extends React.Component {
2531 componentDidMount() {
2532 const {text} = this.props;
2533 Scheduler.log(`ClassText:${text} componentDidMount`);
2534 }
2535 componentDidUpdate() {
2536 const {text} = this.props;
2537 Scheduler.log(`ClassText:${text} componentDidUpdate`);
2538 }
2539 componentWillUnmount() {
2540 const {text} = this.props;
2541 Scheduler.log(`ClassText:${text} componentWillUnmount`);
2542 }
2543 render() {
2544 const {children, text} = this.props;
2545 Scheduler.log(`ClassText:${text} render`);
2546 return <span prop={text}>{children}</span>;
2547 }
2548 }
2549
2550 function App({children = null}) {
2551 return (
2552 <Suspense fallback={<ClassText text="Fallback" />}>
2553 <Text text="Function" />
2554 {children}
2555 <ClassText text="Class" />
2556 </Suspense>
2557 );
2558 }
2559
2560 await act(() => {
2561 ReactNoop.render(<App />);
2562 });
2563 assertLog([
2564 'Text:Function render',
2565 'ClassText:Class render',
2566 'Text:Function create insertion',
2567 'Text:Function create layout',
2568 'ClassText:Class componentDidMount',
2569 'Text:Function create passive',
2570 ]);
2571 expect(ReactNoop).toMatchRenderedOutput(
2572 <>
2573 <span prop="Function" />
2574 <span prop="Class" />
2575 </>,
2576 );
2577
2578 // Schedule an update that causes React to suspend.
2579 await act(async () => {
2580 ReactNoop.render(
2581 <App>
2582 <AsyncText text="Async_1" />
2583 <AsyncText text="Async_2" />
2584 </App>,
2585 );
2586 await waitFor([
2587 'Text:Function render',
2588 'Suspend:Async_1',
2589 'ClassText:Fallback render',
2590 'Text:Function destroy layout',
2591 'ClassText:Class componentWillUnmount',
2592 'ClassText:Fallback componentDidMount',
2593 ]);
2594 expect(ReactNoop).toMatchRenderedOutput(
2595 <>
2596 <span prop="Function" hidden={true} />
2597 <span prop="Class" hidden={true} />
2598 <span prop="Fallback" />
2599 </>,
2600 );
2601 });
2602
2603 // pre-warming
2604 assertLog([
2605 'Text:Function render',
2606 'Suspend:Async_1',
2607 'Suspend:Async_2',
2608 'ClassText:Class render',
2609 ]);
2610
2611 // Resolving the suspended resource should re-create inner layout effects.
2612 await act(async () => {
2613 await resolveText('Async_1');
2614 });
2615 assertLog([
2616 'Text:Function render',
2617 'AsyncText:Async_1 render',
2618 'Suspend:Async_2',
2619 // pre-warming
2620 'Text:Function render',
2621 'AsyncText:Async_1 render',
2622 'Suspend:Async_2',
2623 'ClassText:Class render',
2624 ]);
2625 expect(ReactNoop).toMatchRenderedOutput(
2626 <>
2627 <span prop="Function" hidden={true} />
2628 <span prop="Class" hidden={true} />
2629 <span prop="Fallback" />
2630 </>,
2631 );
2632
2633 // Resolving the suspended resource should re-create inner layout effects.
2634 await act(async () => {
2635 await resolveText('Async_2');
2636 });
2637 assertLog([
2638 'Text:Function render',
2639 'AsyncText:Async_1 render',
2640 'AsyncText:Async_2 render',
2641 'ClassText:Class render',
2642 'ClassText:Fallback componentWillUnmount',
2643 'Text:Function create layout',
2644 'AsyncText:Async_1 create layout',
2645 'AsyncText:Async_2 create layout',
2646 'ClassText:Class componentDidMount',
2647 'AsyncText:Async_1 create passive',
2648 'AsyncText:Async_2 create passive',
2649 ]);
2650 expect(ReactNoop).toMatchRenderedOutput(
2651 <>
2652 <span prop="Function" />
2653 <span prop="Async_1" />
2654 <span prop="Async_2" />
2655 <span prop="Class" />
2656 </>,
2657 );
2658
2659 await act(() => {
2660 ReactNoop.render(null);
2661 });
2662 assertLog([
2663 'Text:Function destroy insertion',
2664 'Text:Function destroy layout',
2665 'AsyncText:Async_1 destroy layout',
2666 'AsyncText:Async_2 destroy layout',
2667 'ClassText:Class componentWillUnmount',
2668 'Text:Function destroy passive',
2669 'AsyncText:Async_1 destroy passive',
2670 'AsyncText:Async_2 destroy passive',
2671 ]);
2672 });
2673
2674 // @gate enableLegacyCache
2675 it('should be only destroy layout effects once if a component suspends multiple times', async () => {
2676 class ClassText extends React.Component {
2677 componentDidMount() {
2678 const {text} = this.props;
2679 Scheduler.log(`ClassText:${text} componentDidMount`);
2680 }
2681 componentDidUpdate() {
2682 const {text} = this.props;
2683 Scheduler.log(`ClassText:${text} componentDidUpdate`);
2684 }
2685 componentWillUnmount() {
2686 const {text} = this.props;
2687 Scheduler.log(`ClassText:${text} componentWillUnmount`);
2688 }
2689 render() {
2690 const {children, text} = this.props;
2691 Scheduler.log(`ClassText:${text} render`);
2692 return <span prop={text}>{children}</span>;
2693 }
2694 }
2695
2696 let textToRead = null;
2697
2698 function Suspender() {
2699 Scheduler.log(`Suspender "${textToRead}" render`);
2700 if (textToRead !== null) {
2701 readText(textToRead);
2702 }
2703 return <span prop="Suspender" />;
2704 }
2705
2706 function App({children = null}) {
2707 return (
2708 <Suspense fallback={<ClassText text="Fallback" />}>
2709 <Text text="Function" />
2710 <Suspender />
2711 <ClassText text="Class" />
2712 </Suspense>
2713 );
2714 }
2715
2716 await act(() => {
2717 ReactNoop.render(<App />);
2718 });
2719 assertLog([
2720 'Text:Function render',
2721 'Suspender "null" render',
2722 'ClassText:Class render',
2723 'Text:Function create insertion',
2724 'Text:Function create layout',
2725 'ClassText:Class componentDidMount',
2726 'Text:Function create passive',
2727 ]);
2728 expect(ReactNoop).toMatchRenderedOutput(
2729 <>
2730 <span prop="Function" />
2731 <span prop="Suspender" />
2732 <span prop="Class" />
2733 </>,
2734 );
2735
2736 // Schedule an update that causes React to suspend.
2737 textToRead = 'A';
2738 await act(async () => {
2739 ReactNoop.render(<App />);
2740 await waitFor([
2741 'Text:Function render',
2742 'Suspender "A" render',
2743 'Suspend:A',
2744 'ClassText:Fallback render',
2745 'Text:Function destroy layout',
2746 'ClassText:Class componentWillUnmount',
2747 'ClassText:Fallback componentDidMount',
2748 ]);
2749 expect(ReactNoop).toMatchRenderedOutput(
2750 <>
2751 <span prop="Function" hidden={true} />
2752 <span prop="Suspender" hidden={true} />
2753 <span prop="Class" hidden={true} />
2754 <span prop="Fallback" />
2755 </>,
2756 );
2757 });
2758
2759 // pre-warming
2760 assertLog([
2761 'Text:Function render',
2762 'Suspender "A" render',
2763 'Suspend:A',
2764 'ClassText:Class render',
2765 ]);
2766
2767 // Resolving the suspended resource should re-create inner layout effects.
2768 textToRead = 'B';
2769 await act(async () => {
2770 await resolveText('A');
2771 });
2772 assertLog([
2773 'Text:Function render',
2774 'Suspender "B" render',
2775 'Suspend:B',
2776 // pre-warming
2777 'Text:Function render',
2778 'Suspender "B" render',
2779 'Suspend:B',
2780 'ClassText:Class render',
2781 ]);
2782 expect(ReactNoop).toMatchRenderedOutput(
2783 <>
2784 <span prop="Function" hidden={true} />
2785 <span prop="Suspender" hidden={true} />
2786 <span prop="Class" hidden={true} />
2787 <span prop="Fallback" />
2788 </>,
2789 );
2790
2791 // Resolving the suspended resource should re-create inner layout effects.
2792 await act(async () => {
2793 await resolveText('B');
2794 });
2795 assertLog([
2796 'Text:Function render',
2797 'Suspender "B" render',
2798 'ClassText:Class render',
2799 'ClassText:Fallback componentWillUnmount',
2800 'Text:Function create layout',
2801 'ClassText:Class componentDidMount',
2802 ]);
2803 expect(ReactNoop).toMatchRenderedOutput(
2804 <>
2805 <span prop="Function" />
2806 <span prop="Suspender" />
2807 <span prop="Class" />
2808 </>,
2809 );
2810
2811 await act(() => {
2812 ReactNoop.render(null);
2813 });
2814 assertLog([
2815 'Text:Function destroy insertion',
2816 'Text:Function destroy layout',
2817 'ClassText:Class componentWillUnmount',
2818 'Text:Function destroy passive',
2819 ]);
2820 });
2821 });
2822
2823 describe('refs within a tree that re-suspends in an update', () => {
2824 function RefCheckerOuter({Component}) {
2825 const refObject = React.useRef(null);
2826
2827 const manualRef = React.useMemo(() => ({current: null}), []);
2828 const refCallback = React.useCallback(value => {
2829 Scheduler.log(`RefCheckerOuter refCallback value? ${value != null}`);
2830 manualRef.current = value;
2831 }, []);
2832
2833 Scheduler.log(`RefCheckerOuter render`);
2834
2835 React.useLayoutEffect(() => {
2836 Scheduler.log(
2837 `RefCheckerOuter create layout refObject? ${
2838 refObject.current != null
2839 } refCallback? ${manualRef.current != null}`,
2840 );
2841 return () => {
2842 Scheduler.log(
2843 `RefCheckerOuter destroy layout refObject? ${
2844 refObject.current != null
2845 } refCallback? ${manualRef.current != null}`,
2846 );
2847 };
2848 }, []);
2849
2850 return (
2851 <>
2852 <Component ref={refObject} prop="refObject">
2853 <RefCheckerInner forwardedRef={refObject} text="refObject" />
2854 </Component>
2855 <Component ref={refCallback} prop="refCallback">
2856 <RefCheckerInner forwardedRef={manualRef} text="refCallback" />
2857 </Component>
2858 </>
2859 );
2860 }
2861
2862 function RefCheckerInner({forwardedRef, text}) {
2863 Scheduler.log(`RefCheckerInner:${text} render`);
2864 React.useLayoutEffect(() => {
2865 Scheduler.log(
2866 `RefCheckerInner:${text} create layout ref? ${
2867 forwardedRef.current != null
2868 }`,
2869 );
2870 return () => {
2871 Scheduler.log(
2872 `RefCheckerInner:${text} destroy layout ref? ${
2873 forwardedRef.current != null
2874 }`,
2875 );
2876 };
2877 }, []);
2878 return null;
2879 }
2880
2881 // @gate enableLegacyCache && !disableLegacyMode
2882 it('should not be cleared within legacy roots', async () => {
2883 class ClassComponent extends React.Component {
2884 render() {
2885 Scheduler.log(`ClassComponent:${this.props.prop} render`);
2886 return this.props.children;
2887 }
2888 }
2889
2890 function App({children}) {
2891 Scheduler.log(`App render`);
2892 return (
2893 <Suspense fallback={<Text text="Fallback" />}>
2894 {children}
2895 <RefCheckerOuter Component={ClassComponent} />
2896 </Suspense>
2897 );
2898 }
2899
2900 await act(() => {
2901 ReactNoop.renderLegacySyncRoot(<App />);
2902 });
2903 assertLog([
2904 'App render',
2905 'RefCheckerOuter render',
2906 'ClassComponent:refObject render',
2907 'RefCheckerInner:refObject render',
2908 'ClassComponent:refCallback render',
2909 'RefCheckerInner:refCallback render',
2910 'RefCheckerInner:refObject create layout ref? false',
2911 'RefCheckerInner:refCallback create layout ref? false',
2912 'RefCheckerOuter refCallback value? true',
2913 'RefCheckerOuter create layout refObject? true refCallback? true',
2914 ]);
2915 expect(ReactNoop).toMatchRenderedOutput(null);
2916
2917 // Suspend the inner Suspense subtree (only inner effects should be destroyed)
2918 await act(() => {
2919 ReactNoop.renderLegacySyncRoot(
2920 <App children={<AsyncText text="Async" />} />,
2921 );
2922 });
2923 await advanceTimers(1000);
2924 assertLog([
2925 'App render',
2926 'Suspend:Async',
2927 'RefCheckerOuter render',
2928 'ClassComponent:refObject render',
2929 'RefCheckerInner:refObject render',
2930 'ClassComponent:refCallback render',
2931 'RefCheckerInner:refCallback render',
2932 'Text:Fallback render',
2933 'Text:Fallback create insertion',
2934 'Text:Fallback create layout',
2935 'Text:Fallback create passive',
2936 ]);
2937 expect(ReactNoop).toMatchRenderedOutput(<span prop="Fallback" />);
2938
2939 // Resolving the suspended resource should re-create inner layout effects.
2940 await act(async () => {
2941 await resolveText('Async');
2942 });
2943 assertLog([
2944 'AsyncText:Async render',
2945 'Text:Fallback destroy insertion',
2946 'Text:Fallback destroy layout',
2947 'AsyncText:Async create layout',
2948 'Text:Fallback destroy passive',
2949 'AsyncText:Async create passive',
2950 ]);
2951 expect(ReactNoop).toMatchRenderedOutput(<span prop="Async" />);
2952
2953 await act(() => {
2954 ReactNoop.renderLegacySyncRoot(null);
2955 });
2956 assertLog([
2957 'AsyncText:Async destroy layout',
2958 'RefCheckerOuter destroy layout refObject? true refCallback? true',
2959 'RefCheckerInner:refObject destroy layout ref? false',
2960 'RefCheckerOuter refCallback value? false',
2961 'RefCheckerInner:refCallback destroy layout ref? false',
2962 'AsyncText:Async destroy passive',
2963 ]);
2964 expect(ReactNoop).toMatchRenderedOutput(null);
2965 });
2966
2967 // @gate enableLegacyCache
2968 it('should be cleared and reset for host components', async () => {
2969 function App({children}) {
2970 Scheduler.log(`App render`);
2971 return (
2972 <Suspense fallback={<Text text="Fallback" />}>
2973 {children}
2974 <RefCheckerOuter Component="span" />
2975 </Suspense>
2976 );
2977 }
2978
2979 // Mount
2980 await act(() => {
2981 ReactNoop.render(<App />);
2982 });
2983 assertLog([
2984 'App render',
2985 'RefCheckerOuter render',
2986 'RefCheckerInner:refObject render',
2987 'RefCheckerInner:refCallback render',
2988 'RefCheckerInner:refObject create layout ref? false',
2989 'RefCheckerInner:refCallback create layout ref? false',
2990 'RefCheckerOuter refCallback value? true',
2991 'RefCheckerOuter create layout refObject? true refCallback? true',
2992 ]);
2993 expect(ReactNoop).toMatchRenderedOutput(
2994 <>
2995 <span prop="refObject" />
2996 <span prop="refCallback" />
2997 </>,
2998 );
2999
3000 // Suspend the inner Suspense subtree (only inner effects should be destroyed)
3001 await act(() => {
3002 ReactNoop.render(<App children={<AsyncText text="Async" />} />);
3003 });
3004 await advanceTimers(1000);
3005 assertLog([
3006 'App render',
3007 'Suspend:Async',
3008 'Text:Fallback render',
3009 'RefCheckerOuter destroy layout refObject? true refCallback? true',
3010 'RefCheckerInner:refObject destroy layout ref? false',
3011 'RefCheckerOuter refCallback value? false',
3012 'RefCheckerInner:refCallback destroy layout ref? false',
3013 'Text:Fallback create insertion',
3014 'Text:Fallback create layout',
3015 'Text:Fallback create passive',
3016 // pre-warming
3017 'Suspend:Async',
3018 'RefCheckerOuter render',
3019 'RefCheckerInner:refObject render',
3020 'RefCheckerInner:refCallback render',
3021 ]);
3022 expect(ReactNoop).toMatchRenderedOutput(
3023 <>
3024 <span prop="refObject" hidden={true} />
3025 <span prop="refCallback" hidden={true} />
3026 <span prop="Fallback" />
3027 </>,
3028 );
3029
3030 // Resolving the suspended resource should re-create inner layout effects.
3031 await act(async () => {
3032 await resolveText('Async');
3033 });
3034 assertLog([
3035 'AsyncText:Async render',
3036 'RefCheckerOuter render',
3037 'RefCheckerInner:refObject render',
3038 'RefCheckerInner:refCallback render',
3039 'Text:Fallback destroy insertion',
3040 'Text:Fallback destroy layout',
3041 'AsyncText:Async create layout',
3042 'RefCheckerInner:refObject create layout ref? false',
3043 'RefCheckerInner:refCallback create layout ref? false',
3044 'RefCheckerOuter refCallback value? true',
3045 'RefCheckerOuter create layout refObject? true refCallback? true',
3046 'Text:Fallback destroy passive',
3047 'AsyncText:Async create passive',
3048 ]);
3049 expect(ReactNoop).toMatchRenderedOutput(
3050 <>
3051 <span prop="Async" />
3052 <span prop="refObject" />
3053 <span prop="refCallback" />
3054 </>,
3055 );
3056
3057 await act(() => {
3058 ReactNoop.render(null);
3059 });
3060 assertLog([
3061 'AsyncText:Async destroy layout',
3062 'RefCheckerOuter destroy layout refObject? true refCallback? true',
3063 'RefCheckerInner:refObject destroy layout ref? false',
3064 'RefCheckerOuter refCallback value? false',
3065 'RefCheckerInner:refCallback destroy layout ref? false',
3066 'AsyncText:Async destroy passive',
3067 ]);
3068 expect(ReactNoop).toMatchRenderedOutput(null);
3069 });
3070
3071 // @gate enableLegacyCache
3072 it('should be cleared and reset for class components', async () => {
3073 class ClassComponent extends React.Component {
3074 render() {
3075 Scheduler.log(`ClassComponent:${this.props.prop} render`);
3076 return this.props.children;
3077 }
3078 }
3079
3080 function App({children}) {
3081 Scheduler.log(`App render`);
3082 return (
3083 <Suspense fallback={<Text text="Fallback" />}>
3084 {children}
3085 <RefCheckerOuter Component={ClassComponent} />
3086 </Suspense>
3087 );
3088 }
3089
3090 // Mount
3091 await act(() => {
3092 ReactNoop.render(<App />);
3093 });
3094 assertLog([
3095 'App render',
3096 'RefCheckerOuter render',
3097 'ClassComponent:refObject render',
3098 'RefCheckerInner:refObject render',
3099 'ClassComponent:refCallback render',
3100 'RefCheckerInner:refCallback render',
3101 'RefCheckerInner:refObject create layout ref? false',
3102 'RefCheckerInner:refCallback create layout ref? false',
3103 'RefCheckerOuter refCallback value? true',
3104 'RefCheckerOuter create layout refObject? true refCallback? true',
3105 ]);
3106 expect(ReactNoop).toMatchRenderedOutput(null);
3107
3108 // Suspend the inner Suspense subtree (only inner effects should be destroyed)
3109 await act(() => {
3110 ReactNoop.render(<App children={<AsyncText text="Async" />} />);
3111 });
3112 await advanceTimers(1000);
3113 assertLog([
3114 'App render',
3115 'Suspend:Async',
3116 'Text:Fallback render',
3117 'RefCheckerOuter destroy layout refObject? true refCallback? true',
3118 'RefCheckerInner:refObject destroy layout ref? false',
3119 'RefCheckerOuter refCallback value? false',
3120 'RefCheckerInner:refCallback destroy layout ref? false',
3121 'Text:Fallback create insertion',
3122 'Text:Fallback create layout',
3123 'Text:Fallback create passive',
3124 // pre-warming
3125 'Suspend:Async',
3126 'RefCheckerOuter render',
3127 'ClassComponent:refObject render',
3128 'RefCheckerInner:refObject render',
3129 'ClassComponent:refCallback render',
3130 'RefCheckerInner:refCallback render',
3131 ]);
3132 expect(ReactNoop).toMatchRenderedOutput(<span prop="Fallback" />);
3133
3134 // Resolving the suspended resource should re-create inner layout effects.
3135 await act(async () => {
3136 await resolveText('Async');
3137 });
3138 assertLog([
3139 'AsyncText:Async render',
3140 'RefCheckerOuter render',
3141 'ClassComponent:refObject render',
3142 'RefCheckerInner:refObject render',
3143 'ClassComponent:refCallback render',
3144 'RefCheckerInner:refCallback render',
3145 'Text:Fallback destroy insertion',
3146 'Text:Fallback destroy layout',
3147 'AsyncText:Async create layout',
3148 'RefCheckerInner:refObject create layout ref? false',
3149 'RefCheckerInner:refCallback create layout ref? false',
3150 'RefCheckerOuter refCallback value? true',
3151 'RefCheckerOuter create layout refObject? true refCallback? true',
3152 'Text:Fallback destroy passive',
3153 'AsyncText:Async create passive',
3154 ]);
3155 expect(ReactNoop).toMatchRenderedOutput(<span prop="Async" />);
3156
3157 await act(() => {
3158 ReactNoop.render(null);
3159 });
3160 assertLog([
3161 'AsyncText:Async destroy layout',
3162 'RefCheckerOuter destroy layout refObject? true refCallback? true',
3163 'RefCheckerInner:refObject destroy layout ref? false',
3164 'RefCheckerOuter refCallback value? false',
3165 'RefCheckerInner:refCallback destroy layout ref? false',
3166 'AsyncText:Async destroy passive',
3167 ]);
3168 expect(ReactNoop).toMatchRenderedOutput(null);
3169 });
3170
3171 // @gate enableLegacyCache
3172 it('should be cleared and reset for function components with useImperativeHandle', async () => {
3173 const FunctionComponent = React.forwardRef((props, ref) => {
3174 Scheduler.log('FunctionComponent render');
3175 React.useImperativeHandle(
3176 ref,
3177 () => ({
3178 // Noop
3179 }),
3180 [],
3181 );
3182 return props.children;
3183 });
3184 FunctionComponent.displayName = 'FunctionComponent';
3185
3186 function App({children}) {
3187 Scheduler.log(`App render`);
3188 return (
3189 <Suspense fallback={<Text text="Fallback" />}>
3190 {children}
3191 <RefCheckerOuter Component={FunctionComponent} />
3192 </Suspense>
3193 );
3194 }
3195
3196 // Mount
3197 await act(() => {
3198 ReactNoop.render(<App />);
3199 });
3200 assertLog([
3201 'App render',
3202 'RefCheckerOuter render',
3203 'FunctionComponent render',
3204 'RefCheckerInner:refObject render',
3205 'FunctionComponent render',
3206 'RefCheckerInner:refCallback render',
3207 'RefCheckerInner:refObject create layout ref? false',
3208 'RefCheckerInner:refCallback create layout ref? false',
3209 'RefCheckerOuter refCallback value? true',
3210 'RefCheckerOuter create layout refObject? true refCallback? true',
3211 ]);
3212 expect(ReactNoop).toMatchRenderedOutput(null);
3213
3214 // Suspend the inner Suspense subtree (only inner effects should be destroyed)
3215 await act(() => {
3216 ReactNoop.render(<App children={<AsyncText text="Async" />} />);
3217 });
3218 await advanceTimers(1000);
3219 assertLog([
3220 'App render',
3221 'Suspend:Async',
3222 'Text:Fallback render',
3223 'RefCheckerOuter destroy layout refObject? true refCallback? true',
3224 'RefCheckerInner:refObject destroy layout ref? false',
3225 'RefCheckerOuter refCallback value? false',
3226 'RefCheckerInner:refCallback destroy layout ref? false',
3227 'Text:Fallback create insertion',
3228 'Text:Fallback create layout',
3229 'Text:Fallback create passive',
3230 // pre-warming
3231 'Suspend:Async',
3232 'RefCheckerOuter render',
3233 'FunctionComponent render',
3234 'RefCheckerInner:refObject render',
3235 'FunctionComponent render',
3236 'RefCheckerInner:refCallback render',
3237 ]);
3238 expect(ReactNoop).toMatchRenderedOutput(<span prop="Fallback" />);
3239
3240 // Resolving the suspended resource should re-create inner layout effects.
3241 await act(async () => {
3242 await resolveText('Async');
3243 });
3244 assertLog([
3245 'AsyncText:Async render',
3246 'RefCheckerOuter render',
3247 'FunctionComponent render',
3248 'RefCheckerInner:refObject render',
3249 'FunctionComponent render',
3250 'RefCheckerInner:refCallback render',
3251 'Text:Fallback destroy insertion',
3252 'Text:Fallback destroy layout',
3253 'AsyncText:Async create layout',
3254 'RefCheckerInner:refObject create layout ref? false',
3255 'RefCheckerInner:refCallback create layout ref? false',
3256 'RefCheckerOuter refCallback value? true',
3257 'RefCheckerOuter create layout refObject? true refCallback? true',
3258 'Text:Fallback destroy passive',
3259 'AsyncText:Async create passive',
3260 ]);
3261 expect(ReactNoop).toMatchRenderedOutput(<span prop="Async" />);
3262
3263 await act(() => {
3264 ReactNoop.render(null);
3265 });
3266 assertLog([
3267 'AsyncText:Async destroy layout',
3268 'RefCheckerOuter destroy layout refObject? true refCallback? true',
3269 'RefCheckerInner:refObject destroy layout ref? false',
3270 'RefCheckerOuter refCallback value? false',
3271 'RefCheckerInner:refCallback destroy layout ref? false',
3272 'AsyncText:Async destroy passive',
3273 ]);
3274 expect(ReactNoop).toMatchRenderedOutput(null);
3275 });
3276
3277 // @gate enableLegacyCache
3278 it('should not reset for user-managed values', async () => {
3279 function RefChecker({forwardedRef}) {
3280 Scheduler.log(`RefChecker render`);
3281 React.useLayoutEffect(() => {
3282 Scheduler.log(
3283 `RefChecker create layout ref? ${forwardedRef.current === 'test'}`,
3284 );
3285 return () => {
3286 Scheduler.log(
3287 `RefChecker destroy layout ref? ${
3288 forwardedRef.current === 'test'
3289 }`,
3290 );
3291 };
3292 }, []);
3293 return null;
3294 }
3295
3296 function App({children = null}) {
3297 const ref = React.useRef('test');
3298 Scheduler.log(`App render`);
3299 React.useLayoutEffect(() => {
3300 Scheduler.log(`App create layout ref? ${ref.current === 'test'}`);
3301 return () => {
3302 Scheduler.log(`App destroy layout ref? ${ref.current === 'test'}`);
3303 };
3304 }, []);
3305 return (
3306 <Suspense fallback={<Text text="Fallback" />}>
3307 {children}
3308 <RefChecker forwardedRef={ref} />
3309 </Suspense>
3310 );
3311 }
3312
3313 // Mount
3314 await act(() => {
3315 ReactNoop.render(<App />);
3316 });
3317 assertLog([
3318 'App render',
3319 'RefChecker render',
3320 'RefChecker create layout ref? true',
3321 'App create layout ref? true',
3322 ]);
3323 expect(ReactNoop).toMatchRenderedOutput(null);
3324
3325 // Suspend the inner Suspense subtree (only inner effects should be destroyed)
3326 await act(() => {
3327 ReactNoop.render(<App children={<AsyncText text="Async" />} />);
3328 });
3329 await advanceTimers(1000);
3330 assertLog([
3331 'App render',
3332 'Suspend:Async',
3333 'Text:Fallback render',
3334 'RefChecker destroy layout ref? true',
3335 'Text:Fallback create insertion',
3336 'Text:Fallback create layout',
3337 'Text:Fallback create passive',
3338 // pre-warming
3339 'Suspend:Async',
3340 'RefChecker render',
3341 ]);
3342 expect(ReactNoop).toMatchRenderedOutput(<span prop="Fallback" />);
3343
3344 // Resolving the suspended resource should re-create inner layout effects.
3345 await act(async () => {
3346 await resolveText('Async');
3347 });
3348 assertLog([
3349 'AsyncText:Async render',
3350 'RefChecker render',
3351 'Text:Fallback destroy insertion',
3352 'Text:Fallback destroy layout',
3353 'AsyncText:Async create layout',
3354 'RefChecker create layout ref? true',
3355 'Text:Fallback destroy passive',
3356 'AsyncText:Async create passive',
3357 ]);
3358 expect(ReactNoop).toMatchRenderedOutput(<span prop="Async" />);
3359
3360 await act(() => {
3361 ReactNoop.render(null);
3362 });
3363 assertLog([
3364 'App destroy layout ref? true',
3365 'AsyncText:Async destroy layout',
3366 'RefChecker destroy layout ref? true',
3367 'AsyncText:Async destroy passive',
3368 ]);
3369 expect(ReactNoop).toMatchRenderedOutput(null);
3370 });
3371
3372 describe('that throw errors', () => {
3373 // @gate enableLegacyCache
3374 it('are properly handled in ref callbacks', async () => {
3375 let useRefCallbackShouldThrow = false;
3376
3377 function ThrowsInRefCallback({unused}) {
3378 Scheduler.log('ThrowsInRefCallback render');
3379 const refCallback = React.useCallback(value => {
3380 Scheduler.log('ThrowsInRefCallback refCallback ref? ' + !!value);
3381 if (useRefCallbackShouldThrow) {
3382 throw Error('expected');
3383 }
3384 }, []);
3385 return <span ref={refCallback} prop="ThrowsInRefCallback" />;
3386 }
3387
3388 function App({children = null}) {
3389 Scheduler.log('App render');
3390 React.useLayoutEffect(() => {
3391 Scheduler.log('App create layout');
3392 return () => {
3393 Scheduler.log('App destroy layout');
3394 };
3395 }, []);
3396 return (
3397 <>
3398 <Suspense fallback={<Text text="Fallback" />}>
3399 {children}
3400 <ThrowsInRefCallback />
3401 <Text text="Inside" />
3402 </Suspense>
3403 <Text text="Outside" />
3404 </>
3405 );
3406 }
3407
3408 await act(() => {
3409 ReactNoop.render(
3410 <ErrorBoundary fallback={<Text text="Error" />}>
3411 <App />
3412 </ErrorBoundary>,
3413 );
3414 });
3415 assertLog([
3416 'ErrorBoundary render: try',
3417 'App render',
3418 'ThrowsInRefCallback render',
3419 'Text:Inside render',
3420 'Text:Outside render',
3421 'Text:Inside create insertion',
3422 'Text:Outside create insertion',
3423 'ThrowsInRefCallback refCallback ref? true',
3424 'Text:Inside create layout',
3425 'Text:Outside create layout',
3426 'App create layout',
3427 'Text:Inside create passive',
3428 'Text:Outside create passive',
3429 ]);
3430 expect(ReactNoop).toMatchRenderedOutput(
3431 <>
3432 <span prop="ThrowsInRefCallback" />
3433 <span prop="Inside" />
3434 <span prop="Outside" />
3435 </>,
3436 );
3437
3438 // Schedule an update that causes React to suspend.
3439 await act(() => {
3440 ReactNoop.render(
3441 <ErrorBoundary fallback={<Text text="Error" />}>
3442 <App>
3443 <AsyncText text="Async" />
3444 </App>
3445 </ErrorBoundary>,
3446 );
3447 });
3448 assertLog([
3449 'ErrorBoundary render: try',
3450 'App render',
3451 'Suspend:Async',
3452 'Text:Fallback render',
3453 'Text:Outside render',
3454 'ThrowsInRefCallback refCallback ref? false',
3455 'Text:Inside destroy layout',
3456 'Text:Fallback create insertion',
3457 'Text:Fallback create layout',
3458 'Text:Fallback create passive',
3459 // pre-warming
3460 'Suspend:Async',
3461 'ThrowsInRefCallback render',
3462 'Text:Inside render',
3463 ]);
3464 expect(ReactNoop).toMatchRenderedOutput(
3465 <>
3466 <span prop="ThrowsInRefCallback" hidden={true} />
3467 <span prop="Inside" hidden={true} />
3468 <span prop="Fallback" />
3469 <span prop="Outside" />
3470 </>,
3471 );
3472
3473 // Resolve the pending suspense and throw
3474 useRefCallbackShouldThrow = true;
3475 await act(async () => {
3476 await resolveText('Async');
3477 });
3478 assertLog([
3479 'AsyncText:Async render',
3480 'ThrowsInRefCallback render',
3481 'Text:Inside render',
3482
3483 // Even though an error was thrown in refCallback,
3484 // subsequent layout effects should still be created.
3485 'Text:Fallback destroy insertion',
3486 'Text:Fallback destroy layout',
3487 'AsyncText:Async create layout',
3488 'ThrowsInRefCallback refCallback ref? true',
3489 'Text:Inside create layout',
3490
3491 // Finish the in-progress commit
3492 'Text:Fallback destroy passive',
3493 'AsyncText:Async create passive',
3494
3495 // Destroy insertion, layout, and passive effects in the errored tree.
3496 'App destroy layout',
3497 'AsyncText:Async destroy layout',
3498 'ThrowsInRefCallback refCallback ref? false',
3499 'Text:Inside destroy insertion',
3500 'Text:Inside destroy layout',
3501 'Text:Outside destroy insertion',
3502 'Text:Outside destroy layout',
3503 'AsyncText:Async destroy passive',
3504 'Text:Inside destroy passive',
3505 'Text:Outside destroy passive',
3506
3507 // Render fallback
3508 'ErrorBoundary render: catch',
3509 'Text:Error render',
3510 'Text:Error create insertion',
3511 'Text:Error create layout',
3512 'Text:Error create passive',
3513 ]);
3514 expect(ReactNoop).toMatchRenderedOutput(<span prop="Error" />);
3515 });
3516 });
3517 });
3518 });