main
js 1,462 lines 40 KB
Raw
1 let React;
2 let ReactDOMClient;
3 let ReactDOM;
4 let Scheduler;
5 let Suspense;
6 let act;
7 let textCache;
8 let container;
9
10 let assertLog;
11 let waitForPaint;
12 let waitForAll;
13 let waitFor;
14
15 describe('ReactSuspense', () => {
16 beforeEach(() => {
17 jest.resetModules();
18 React = require('react');
19 ReactDOM = require('react-dom');
20 ReactDOMClient = require('react-dom/client');
21 act = require('internal-test-utils').act;
22 Scheduler = require('scheduler');
23 container = document.createElement('div');
24
25 Suspense = React.Suspense;
26
27 const InternalTestUtils = require('internal-test-utils');
28 waitForAll = InternalTestUtils.waitForAll;
29 waitForPaint = InternalTestUtils.waitForPaint;
30 assertLog = InternalTestUtils.assertLog;
31 waitFor = InternalTestUtils.waitFor;
32
33 textCache = new Map();
34 });
35
36 function resolveText(text) {
37 const record = textCache.get(text);
38 if (record === undefined) {
39 const newRecord = {
40 status: 'resolved',
41 value: text,
42 };
43 textCache.set(text, newRecord);
44 } else if (record.status === 'pending') {
45 const thenable = record.value;
46 record.status = 'resolved';
47 record.value = text;
48 thenable.pings.forEach(t => t());
49 }
50 }
51
52 function readText(text) {
53 const record = textCache.get(text);
54 if (record !== undefined) {
55 switch (record.status) {
56 case 'pending':
57 Scheduler.log(`Suspend! [${text}]`);
58 throw record.value;
59 case 'rejected':
60 throw record.value;
61 case 'resolved':
62 return record.value;
63 }
64 } else {
65 Scheduler.log(`Suspend! [${text}]`);
66 const thenable = {
67 pings: [],
68 then(resolve) {
69 if (newRecord.status === 'pending') {
70 thenable.pings.push(resolve);
71 } else {
72 Promise.resolve().then(() => resolve(newRecord.value));
73 }
74 },
75 };
76
77 const newRecord = {
78 status: 'pending',
79 value: thenable,
80 };
81 textCache.set(text, newRecord);
82
83 throw thenable;
84 }
85 }
86
87 function Text({text}) {
88 Scheduler.log(text);
89 return text;
90 }
91
92 function AsyncText({text}) {
93 readText(text);
94 Scheduler.log(text);
95 return text;
96 }
97
98 it('suspends rendering and continues later', async () => {
99 function Bar(props) {
100 Scheduler.log('Bar');
101 return props.children;
102 }
103
104 function Foo({renderBar}) {
105 Scheduler.log('Foo');
106 return (
107 <Suspense fallback={<Text text="Loading..." />}>
108 {renderBar ? (
109 <Bar>
110 <AsyncText text="A" />
111 <Text text="B" />
112 </Bar>
113 ) : null}
114 </Suspense>
115 );
116 }
117
118 const root = ReactDOMClient.createRoot(container);
119 // Render an empty shell
120 await act(() => {
121 root.render(<Foo />);
122 });
123 assertLog(['Foo']);
124 const renderedEl = container;
125 expect(renderedEl.innerText).toBeUndefined();
126
127 // Navigate the shell to now render the child content.
128 // This should suspend.
129 React.startTransition(() => {
130 root.render(<Foo renderBar={true} />);
131 });
132
133 await waitForAll([
134 'Foo',
135 'Bar',
136 // A suspends
137 'Suspend! [A]',
138 // pre-warming
139 'B',
140 // end pre-warming
141 'Loading...',
142 ]);
143 expect(container.textContent).toEqual('');
144
145 await waitForAll([]);
146 expect(container.textContent).toEqual('');
147
148 resolveText('A');
149 await waitForAll(['Foo', 'Bar', 'A', 'B']);
150 expect(container.textContent).toEqual('AB');
151 });
152
153 it('suspends siblings and later recovers each independently', async () => {
154 const root = ReactDOMClient.createRoot(container);
155 // Render two sibling Suspense components
156 root.render(
157 <>
158 <Suspense fallback={<Text text="Loading A..." />}>
159 <AsyncText text="A" />
160 </Suspense>
161 <Suspense fallback={<Text text="Loading B..." />}>
162 <AsyncText text="B" />
163 </Suspense>
164 </>,
165 );
166
167 await waitForAll([
168 'Suspend! [A]',
169 'Loading A...',
170 'Suspend! [B]',
171 'Loading B...',
172 // pre-warming
173 'Suspend! [A]',
174 'Suspend! [B]',
175 ]);
176 expect(container.innerHTML).toEqual('Loading A...Loading B...');
177
178 // Resolve first Suspense's promise and switch back to the normal view. The
179 // second Suspense should still show the placeholder
180 await act(() => resolveText('A'));
181 assertLog([
182 'A',
183 ...(gate('alwaysThrottleRetries')
184 ? ['Suspend! [B]', 'Suspend! [B]']
185 : []),
186 ]);
187 expect(container.textContent).toEqual('ALoading B...');
188
189 // Resolve the second Suspense's promise resolves and switche back to the
190 // normal view
191 await act(() => resolveText('B'));
192 assertLog(['B']);
193 expect(container.textContent).toEqual('AB');
194 });
195
196 it('interrupts current render if promise resolves before current render phase', async () => {
197 let didResolve = false;
198 const listeners = [];
199
200 const thenable = {
201 then(resolve) {
202 if (!didResolve) {
203 listeners.push(resolve);
204 } else {
205 resolve();
206 }
207 },
208 };
209
210 function resolveThenable() {
211 didResolve = true;
212 listeners.forEach(l => l());
213 }
214
215 function Async() {
216 if (!didResolve) {
217 Scheduler.log('Suspend!');
218 throw thenable;
219 }
220 Scheduler.log('Async');
221 return 'Async';
222 }
223 const root = ReactDOMClient.createRoot(container);
224 await act(() => {
225 root.render(
226 <>
227 <Suspense fallback={<Text text="Loading..." />} />
228 <Text text="Initial" />
229 </>,
230 );
231 });
232
233 assertLog(['Initial']);
234 expect(container.textContent).toEqual('Initial');
235
236 // The update will suspend.
237 React.startTransition(() => {
238 root.render(
239 <>
240 <Suspense fallback={<Text text="Loading..." />}>
241 <Async />
242 </Suspense>
243 <Text text="After Suspense" />
244 <Text text="Sibling" />
245 </>,
246 );
247 });
248
249 // Yield past the Suspense boundary but don't complete the last sibling.
250 await waitFor(['Suspend!', 'Loading...', 'After Suspense']);
251
252 // The promise resolves before the current render phase has completed
253 resolveThenable();
254 assertLog([]);
255 expect(container.textContent).toEqual('Initial');
256
257 // Start over from the root, instead of continuing.
258 await waitForAll([
259 // Async renders again *before* Sibling
260 'Async',
261 'After Suspense',
262 'Sibling',
263 ]);
264 expect(container.textContent).toEqual('AsyncAfter SuspenseSibling');
265 });
266
267 it('throttles fallback committing globally', async () => {
268 function Foo() {
269 Scheduler.log('Foo');
270 return (
271 <Suspense fallback={<Text text="Loading..." />}>
272 <AsyncText text="A" />
273 <Suspense fallback={<Text text="Loading more..." />}>
274 <AsyncText text="B" />
275 </Suspense>
276 </Suspense>
277 );
278 }
279
280 const root = ReactDOMClient.createRoot(container);
281 await act(() => {
282 root.render(<Foo />);
283 });
284
285 assertLog([
286 'Foo',
287 'Suspend! [A]',
288 'Loading...',
289 // pre-warming
290 'Suspend! [A]',
291 'Suspend! [B]',
292 'Loading more...',
293 ]);
294 expect(container.textContent).toEqual('Loading...');
295
296 await resolveText('A');
297 await waitForAll(['A', 'Suspend! [B]', 'Loading more...']);
298
299 // By this point, we have enough info to show "A" and "Loading more..."
300 // However, we've just shown the outer fallback. So we'll delay
301 // showing the inner fallback hoping that B will resolve soon enough.
302 expect(container.textContent).toEqual('Loading...');
303
304 await act(() => resolveText('B'));
305 // By this point, B has resolved.
306 // The contents of both should pop in together.
307 assertLog(['A', 'B']);
308 expect(container.textContent).toEqual('AB');
309 });
310
311 it('pushes out siblings that render faster than throttle', async () => {
312 function Foo() {
313 Scheduler.log('Foo');
314 return (
315 <Suspense fallback={<Text text="Loading..." />}>
316 <AsyncText text="A" ms={290} />
317 <Suspense fallback={<Text text="Loading more..." />}>
318 <AsyncText text="B" ms={30} />
319 </Suspense>
320 </Suspense>
321 );
322 }
323
324 setTimeout(async () => {
325 // TODO: this is dumb, but AsyncText isn't timer based after the act changes.
326 // Pretend that this is the start of the sibling suspending.
327 // In a real test, the timer would start when we render B.
328 setTimeout(async () => {
329 resolveText('B');
330 }, 30);
331
332 resolveText('A');
333 }, 290);
334
335 // Render an empty shell
336 const root = ReactDOMClient.createRoot(container);
337 root.render(<Foo />);
338 await waitForAll([
339 'Foo',
340 'Suspend! [A]',
341 'Loading...',
342 // pre-warming
343 'Suspend! [A]',
344 'Suspend! [B]',
345 'Loading more...',
346 ]);
347 expect(container.textContent).toEqual('Loading...');
348
349 // Now resolve A
350 jest.advanceTimersByTime(290);
351 await waitFor(['A']);
352 expect(container.textContent).toEqual('Loading...');
353
354 // B starts loading. Parent boundary is in throttle.
355 // Still shows parent loading under throttle
356 jest.advanceTimersByTime(10);
357 await waitForAll(['Suspend! [B]', 'Loading more...']);
358 expect(container.textContent).toEqual('Loading...');
359
360 // !! B could have finished before the throttle, but we show a fallback.
361 // !! Pushing out the 30ms fetch for B to 300ms.
362 jest.advanceTimersByTime(300);
363 await waitFor(['B']);
364 expect(container.textContent).toEqual('ALoading more...');
365
366 await act(() => {});
367 expect(container.textContent).toEqual('AB');
368 });
369
370 it('does not throttle fallback committing for too long', async () => {
371 function Foo() {
372 Scheduler.log('Foo');
373 return (
374 <Suspense fallback={<Text text="Loading..." />}>
375 <AsyncText text="A" />
376 <Suspense fallback={<Text text="Loading more..." />}>
377 <AsyncText text="B" />
378 </Suspense>
379 </Suspense>
380 );
381 }
382
383 const root = ReactDOMClient.createRoot(container);
384 await act(() => {
385 root.render(<Foo />);
386 });
387 assertLog([
388 'Foo',
389 'Suspend! [A]',
390 'Loading...',
391 // pre-warming
392 'Suspend! [A]',
393 'Suspend! [B]',
394 'Loading more...',
395 ]);
396 expect(container.textContent).toEqual('Loading...');
397
398 await resolveText('A');
399 await waitForAll(['A', 'Suspend! [B]', 'Loading more...']);
400
401 // By this point, we have enough info to show "A" and "Loading more..."
402 // However, we've just shown the outer fallback. So we'll delay
403 // showing the inner fallback hoping that B will resolve soon enough.
404 expect(container.textContent).toEqual('Loading...');
405 // But if we wait a bit longer, eventually we'll give up and show a
406 // fallback. The exact value here isn't important. It's a JND ("Just
407 // Noticeable Difference").
408 jest.advanceTimersByTime(500);
409 expect(container.textContent).toEqual('ALoading more...');
410
411 await act(() => resolveText('B'));
412 assertLog(['B']);
413 expect(container.textContent).toEqual('AB');
414 });
415
416 // @gate !disableLegacyMode
417 it('mounts a lazy class component in non-concurrent mode (legacy)', async () => {
418 class Class extends React.Component {
419 componentDidMount() {
420 Scheduler.log('Did mount: ' + this.props.label);
421 }
422 componentDidUpdate() {
423 Scheduler.log('Did update: ' + this.props.label);
424 }
425 render() {
426 return <Text text={this.props.label} />;
427 }
428 }
429
430 async function fakeImport(result) {
431 return {default: result};
432 }
433
434 const LazyClass = React.lazy(() => fakeImport(Class));
435
436 ReactDOM.render(
437 <Suspense fallback={<Text text="Loading..." />}>
438 <LazyClass label="Hi" />
439 </Suspense>,
440 container,
441 );
442
443 assertLog(['Loading...']);
444 expect(container.textContent).toEqual('Loading...');
445
446 await LazyClass;
447
448 await waitForPaint(['Hi', 'Did mount: Hi']);
449 expect(container.textContent).toEqual('Hi');
450 });
451
452 it('updates memoized child of suspense component when context updates (simple memo)', async () => {
453 const {useContext, createContext, useState, memo} = React;
454
455 const ValueContext = createContext(null);
456
457 const MemoizedChild = memo(function MemoizedChild() {
458 const text = useContext(ValueContext);
459 return <Text text={readText(text)} />;
460 });
461
462 let setValue;
463 function App() {
464 const [value, _setValue] = useState('default');
465 setValue = _setValue;
466
467 return (
468 <ValueContext.Provider value={value}>
469 <Suspense fallback={<Text text="Loading..." />}>
470 <MemoizedChild />
471 </Suspense>
472 </ValueContext.Provider>
473 );
474 }
475
476 const root = ReactDOMClient.createRoot(container);
477 await act(() => {
478 root.render(<App />);
479 });
480 assertLog([
481 'Suspend! [default]',
482 'Loading...',
483 // pre-warming
484 'Suspend! [default]',
485 ]);
486
487 await act(() => resolveText('default'));
488 assertLog(['default']);
489 expect(container.textContent).toEqual('default');
490
491 await act(() => setValue('new value'));
492 assertLog([
493 'Suspend! [new value]',
494 'Loading...',
495 // pre-warming
496 'Suspend! [new value]',
497 ]);
498
499 await act(() => resolveText('new value'));
500 assertLog(['new value']);
501 expect(container.textContent).toEqual('new value');
502 });
503
504 it('updates memoized child of suspense component when context updates (manual memo)', async () => {
505 const {useContext, createContext, useState, memo} = React;
506
507 const ValueContext = createContext(null);
508
509 const MemoizedChild = memo(
510 function MemoizedChild() {
511 const text = useContext(ValueContext);
512 return <Text text={readText(text)} />;
513 },
514 function areEqual(prevProps, nextProps) {
515 return true;
516 },
517 );
518
519 let setValue;
520 function App() {
521 const [value, _setValue] = useState('default');
522 setValue = _setValue;
523
524 return (
525 <ValueContext.Provider value={value}>
526 <Suspense fallback={<Text text="Loading..." />}>
527 <MemoizedChild />
528 </Suspense>
529 </ValueContext.Provider>
530 );
531 }
532
533 const root = ReactDOMClient.createRoot(container);
534 await act(() => {
535 root.render(<App />);
536 });
537 assertLog([
538 'Suspend! [default]',
539 'Loading...',
540 // pre-warming
541 'Suspend! [default]',
542 ]);
543
544 await act(() => resolveText('default'));
545 assertLog(['default']);
546 expect(container.textContent).toEqual('default');
547
548 await act(() => setValue('new value'));
549 assertLog([
550 'Suspend! [new value]',
551 'Loading...',
552 // pre-warming
553 'Suspend! [new value]',
554 ]);
555
556 await act(() => resolveText('new value'));
557 assertLog(['new value']);
558 expect(container.textContent).toEqual('new value');
559 });
560
561 it('updates memoized child of suspense component when context updates (function)', async () => {
562 const {useContext, createContext, useState} = React;
563
564 const ValueContext = createContext(null);
565
566 function MemoizedChild() {
567 const text = useContext(ValueContext);
568 return <Text text={readText(text)} />;
569 }
570
571 let setValue;
572 function App({children}) {
573 const [value, _setValue] = useState('default');
574 setValue = _setValue;
575
576 return (
577 <ValueContext.Provider value={value}>{children}</ValueContext.Provider>
578 );
579 }
580
581 const root = ReactDOMClient.createRoot(container);
582 await act(() => {
583 root.render(
584 <App>
585 <Suspense fallback={<Text text="Loading..." />}>
586 <MemoizedChild />
587 </Suspense>
588 </App>,
589 );
590 });
591 assertLog([
592 'Suspend! [default]',
593 'Loading...',
594 // pre-warming
595 'Suspend! [default]',
596 ]);
597
598 await act(() => resolveText('default'));
599 assertLog(['default']);
600 expect(container.textContent).toEqual('default');
601
602 await act(() => setValue('new value'));
603 assertLog([
604 'Suspend! [new value]',
605 'Loading...',
606 // pre-warming
607 'Suspend! [new value]',
608 ]);
609
610 await act(() => resolveText('new value'));
611 assertLog(['new value']);
612 expect(container.textContent).toEqual('new value');
613 });
614
615 it('updates memoized child of suspense component when context updates (forwardRef)', async () => {
616 const {forwardRef, useContext, createContext, useState} = React;
617
618 const ValueContext = createContext(null);
619
620 const MemoizedChild = forwardRef(() => {
621 const text = useContext(ValueContext);
622 return <Text text={readText(text)} />;
623 });
624
625 let setValue;
626 function App({children}) {
627 const [value, _setValue] = useState('default');
628 setValue = _setValue;
629
630 return (
631 <ValueContext.Provider value={value}>{children}</ValueContext.Provider>
632 );
633 }
634
635 const root = ReactDOMClient.createRoot(container);
636 await act(() => {
637 root.render(
638 <App>
639 <Suspense fallback={<Text text="Loading..." />}>
640 <MemoizedChild />
641 </Suspense>
642 </App>,
643 );
644 });
645 assertLog([
646 'Suspend! [default]',
647 'Loading...',
648 // pre-warming
649 'Suspend! [default]',
650 ]);
651
652 await act(() => resolveText('default'));
653 assertLog(['default']);
654 expect(container.textContent).toEqual('default');
655
656 await act(() => setValue('new value'));
657 assertLog([
658 'Suspend! [new value]',
659 'Loading...',
660 // pre-warming
661 'Suspend! [new value]',
662 ]);
663
664 await act(() => resolveText('new value'));
665 assertLog(['new value']);
666 expect(container.textContent).toEqual('new value');
667 });
668
669 it('re-fires layout effects when re-showing Suspense', async () => {
670 function TextWithLayout(props) {
671 Scheduler.log(props.text);
672 React.useLayoutEffect(() => {
673 Scheduler.log('create layout');
674 return () => {
675 Scheduler.log('destroy layout');
676 };
677 }, []);
678 return props.text;
679 }
680
681 let _setShow;
682 function App(props) {
683 const [show, setShow] = React.useState(false);
684 _setShow = setShow;
685 return (
686 <Suspense fallback={<Text text="Loading..." />}>
687 <TextWithLayout text="Child 1" />
688 {show && <AsyncText text="Child 2" />}
689 </Suspense>
690 );
691 }
692
693 const root = ReactDOMClient.createRoot(container);
694 await act(() => {
695 root.render(<App />);
696 });
697
698 assertLog(['Child 1', 'create layout']);
699 expect(container.textContent).toEqual('Child 1');
700
701 await act(() => {
702 _setShow(true);
703 });
704 assertLog([
705 'Child 1',
706 'Suspend! [Child 2]',
707 'Loading...',
708 'destroy layout',
709 // pre-warming
710 'Child 1',
711 'Suspend! [Child 2]',
712 ]);
713
714 await act(() => resolveText('Child 2'));
715 assertLog(['Child 1', 'Child 2', 'create layout']);
716 expect(container.textContent).toEqual(['Child 1', 'Child 2'].join(''));
717 });
718
719 it('does not get stuck with fallback in concurrent mode for a large delay', async () => {
720 function App(props) {
721 return (
722 <Suspense fallback={<Text text="Loading..." />}>
723 <AsyncText text="Child 1" />
724 <AsyncText text="Child 2" />
725 </Suspense>
726 );
727 }
728
729 const root = ReactDOMClient.createRoot(container);
730 await act(() => {
731 root.render(<App />);
732 });
733
734 assertLog([
735 'Suspend! [Child 1]',
736 'Loading...',
737 // pre-warming
738 'Suspend! [Child 1]',
739 'Suspend! [Child 2]',
740 ]);
741 await resolveText('Child 1');
742 await waitForAll([
743 'Child 1',
744 'Suspend! [Child 2]',
745 ...(gate('alwaysThrottleRetries')
746 ? []
747 : ['Child 1', 'Suspend! [Child 2]']),
748 ]);
749
750 jest.advanceTimersByTime(6000);
751
752 await act(() => resolveText('Child 2'));
753 assertLog(['Child 1', 'Child 2']);
754 expect(container.textContent).toEqual(['Child 1', 'Child 2'].join(''));
755 });
756
757 describe('outside concurrent mode (legacy)', () => {
758 // @gate !disableLegacyMode
759 it('a mounted class component can suspend without losing state', async () => {
760 class TextWithLifecycle extends React.Component {
761 componentDidMount() {
762 Scheduler.log(`Mount [${this.props.text}]`);
763 }
764 componentDidUpdate() {
765 Scheduler.log(`Update [${this.props.text}]`);
766 }
767 componentWillUnmount() {
768 Scheduler.log(`Unmount [${this.props.text}]`);
769 }
770 render() {
771 return <Text {...this.props} />;
772 }
773 }
774
775 let instance;
776 class AsyncTextWithLifecycle extends React.Component {
777 state = {step: 1};
778 componentDidMount() {
779 Scheduler.log(`Mount [${this.props.text}:${this.state.step}]`);
780 }
781 componentDidUpdate() {
782 Scheduler.log(`Update [${this.props.text}:${this.state.step}]`);
783 }
784 componentWillUnmount() {
785 Scheduler.log(`Unmount [${this.props.text}:${this.state.step}]`);
786 }
787 render() {
788 instance = this;
789 const text = readText(`${this.props.text}:${this.state.step}`);
790 return <Text text={text} />;
791 }
792 }
793
794 function App() {
795 return (
796 <Suspense fallback={<TextWithLifecycle text="Loading..." />}>
797 <TextWithLifecycle text="A" />
798 <AsyncTextWithLifecycle text="B" ref={instance} />
799 <TextWithLifecycle text="C" />
800 </Suspense>
801 );
802 }
803
804 ReactDOM.render(<App />, container);
805 assertLog([
806 'A',
807 'Suspend! [B:1]',
808 'C',
809 'Loading...',
810
811 'Mount [A]',
812 // B's lifecycle should not fire because it suspended
813 // 'Mount [B]',
814 'Mount [C]',
815 'Mount [Loading...]',
816 ]);
817 expect(container.textContent).toEqual('Loading...');
818
819 await resolveText('B:1');
820 assertLog([
821 'B:1',
822 'Unmount [Loading...]',
823 // Should be a mount, not an update
824 'Mount [B:1]',
825 ]);
826 expect(container.textContent).toEqual('AB:1C');
827
828 instance.setState({step: 2});
829 assertLog(['Suspend! [B:2]', 'Loading...', 'Mount [Loading...]']);
830 expect(container.textContent).toEqual('Loading...');
831
832 await resolveText('B:2');
833 assertLog(['B:2', 'Unmount [Loading...]', 'Update [B:2]']);
834 expect(container.textContent).toEqual('AB:2C');
835 });
836
837 // @gate !disableLegacyMode
838 it('bails out on timed-out primary children even if they receive an update', async () => {
839 let instance;
840 class Stateful extends React.Component {
841 state = {step: 1};
842 render() {
843 instance = this;
844 return <Text text={`Stateful: ${this.state.step}`} />;
845 }
846 }
847
848 function App(props) {
849 return (
850 <Suspense fallback={<Text text="Loading..." />}>
851 <Stateful />
852 <AsyncText text={props.text} />
853 </Suspense>
854 );
855 }
856
857 ReactDOM.render(<App text="A" />, container);
858
859 assertLog(['Stateful: 1', 'Suspend! [A]', 'Loading...']);
860
861 await resolveText('A');
862 assertLog(['A']);
863 expect(container.textContent).toEqual('Stateful: 1A');
864
865 ReactDOM.render(<App text="B" />, container);
866 assertLog(['Stateful: 1', 'Suspend! [B]', 'Loading...']);
867 expect(container.textContent).toEqual('Loading...');
868
869 instance.setState({step: 2});
870 assertLog(['Stateful: 2', 'Suspend! [B]']);
871 expect(container.textContent).toEqual('Loading...');
872
873 await resolveText('B');
874 assertLog(['B']);
875 expect(container.textContent).toEqual('Stateful: 2B');
876 });
877
878 // @gate !disableLegacyMode
879 it('when updating a timed-out tree, always retries the suspended component', async () => {
880 let instance;
881 class Stateful extends React.Component {
882 state = {step: 1};
883 render() {
884 instance = this;
885 return <Text text={`Stateful: ${this.state.step}`} />;
886 }
887 }
888
889 const Indirection = React.Fragment;
890
891 function App(props) {
892 return (
893 <Suspense fallback={<Text text="Loading..." />}>
894 <Stateful />
895 <Indirection>
896 <Indirection>
897 <Indirection>
898 <AsyncText text={props.text} />
899 </Indirection>
900 </Indirection>
901 </Indirection>
902 </Suspense>
903 );
904 }
905
906 ReactDOM.render(<App text="A" />, container);
907
908 assertLog(['Stateful: 1', 'Suspend! [A]', 'Loading...']);
909
910 await resolveText('A');
911 assertLog(['A']);
912 expect(container.textContent).toEqual('Stateful: 1A');
913
914 ReactDOM.render(<App text="B" />, container);
915 assertLog(['Stateful: 1', 'Suspend! [B]', 'Loading...']);
916 expect(container.textContent).toEqual('Loading...');
917
918 instance.setState({step: 2});
919 assertLog([
920 'Stateful: 2',
921
922 // The suspended component should suspend again. If it doesn't, the
923 // likely mistake is that the suspended fiber wasn't marked with
924 // pending work, so it was improperly treated as complete.
925 'Suspend! [B]',
926 ]);
927 expect(container.textContent).toEqual('Loading...');
928
929 await resolveText('B');
930 assertLog(['B']);
931 expect(container.textContent).toEqual('Stateful: 2B');
932 });
933
934 // @gate !disableLegacyMode
935 it('suspends in a class that has componentWillUnmount and is then deleted', async () => {
936 class AsyncTextWithUnmount extends React.Component {
937 componentWillUnmount() {
938 Scheduler.log('will unmount');
939 }
940 render() {
941 return <Text text={readText(this.props.text)} />;
942 }
943 }
944
945 function App({text}) {
946 return (
947 <Suspense fallback={<Text text="Loading..." />}>
948 <AsyncTextWithUnmount text={text} />
949 </Suspense>
950 );
951 }
952
953 ReactDOM.render(<App text="A" />, container);
954 assertLog(['Suspend! [A]', 'Loading...']);
955 ReactDOM.render(<Text text="B" />, container);
956 // Should not fire componentWillUnmount
957 assertLog(['B']);
958 expect(container.textContent).toEqual('B');
959 });
960
961 // @gate !disableLegacyMode
962 it('suspends in a component that also contains useEffect', async () => {
963 const {useLayoutEffect} = React;
964
965 function AsyncTextWithEffect(props) {
966 const text = props.text;
967
968 useLayoutEffect(() => {
969 Scheduler.log('Did commit: ' + text);
970 }, [text]);
971
972 return <Text text={readText(text)} />;
973 }
974
975 function App({text}) {
976 return (
977 <Suspense fallback={<Text text="Loading..." />}>
978 <AsyncTextWithEffect text={text} />
979 </Suspense>
980 );
981 }
982
983 ReactDOM.render(<App text="A" />, container);
984 assertLog(['Suspend! [A]', 'Loading...']);
985 await resolveText('A');
986 assertLog(['A', 'Did commit: A']);
987 });
988
989 // @gate !disableLegacyMode
990 it('retries when an update is scheduled on a timed out tree', async () => {
991 let instance;
992 class Stateful extends React.Component {
993 state = {step: 1};
994 render() {
995 instance = this;
996 return <AsyncText text={`Step: ${this.state.step}`} />;
997 }
998 }
999
1000 function App(props) {
1001 return (
1002 <Suspense fallback={<Text text="Loading..." />}>
1003 <Stateful />
1004 </Suspense>
1005 );
1006 }
1007
1008 ReactDOM.render(<App />, container);
1009
1010 // Initial render
1011 assertLog(['Suspend! [Step: 1]', 'Loading...']);
1012
1013 await act(() => resolveText('Step: 1'));
1014 assertLog(['Step: 1']);
1015 expect(container.textContent).toEqual('Step: 1');
1016
1017 // Update that suspends
1018 await act(() => {
1019 instance.setState({step: 2});
1020 });
1021 assertLog(['Suspend! [Step: 2]', 'Loading...']);
1022 expect(container.textContent).toEqual('Loading...');
1023
1024 // Update while still suspended
1025 instance.setState({step: 3});
1026 assertLog(['Suspend! [Step: 3]']);
1027 expect(container.textContent).toEqual('Loading...');
1028
1029 await act(() => {
1030 resolveText('Step: 2');
1031 resolveText('Step: 3');
1032 });
1033 assertLog(['Step: 3']);
1034 expect(container.textContent).toEqual('Step: 3');
1035 });
1036
1037 // @gate !disableLegacyMode
1038 it('does not remount the fallback while suspended children resolve in legacy mode', async () => {
1039 let mounts = 0;
1040 class ShouldMountOnce extends React.Component {
1041 componentDidMount() {
1042 mounts++;
1043 }
1044 render() {
1045 return <Text text="Loading..." />;
1046 }
1047 }
1048
1049 function App(props) {
1050 return (
1051 <Suspense fallback={<ShouldMountOnce />}>
1052 <AsyncText text="Child 1" />
1053 <AsyncText text="Child 2" />
1054 <AsyncText text="Child 3" />
1055 </Suspense>
1056 );
1057 }
1058
1059 ReactDOM.render(<App />, container);
1060
1061 // Initial render
1062 assertLog([
1063 'Suspend! [Child 1]',
1064 'Suspend! [Child 2]',
1065 'Suspend! [Child 3]',
1066 'Loading...',
1067 ]);
1068 await waitForAll([]);
1069
1070 await resolveText('Child 1');
1071 assertLog(['Child 1', 'Suspend! [Child 2]', 'Suspend! [Child 3]']);
1072
1073 await resolveText('Child 2');
1074 assertLog(['Child 2', 'Suspend! [Child 3]']);
1075
1076 await resolveText('Child 3');
1077 assertLog(['Child 3']);
1078 expect(container.textContent).toEqual(
1079 ['Child 1', 'Child 2', 'Child 3'].join(''),
1080 );
1081 expect(mounts).toBe(1);
1082 });
1083
1084 // @gate !disableLegacyMode
1085 it('reuses effects, including deletions, from the suspended tree', async () => {
1086 const {useState} = React;
1087
1088 let setTab;
1089 function App() {
1090 const [tab, _setTab] = useState(0);
1091 setTab = _setTab;
1092
1093 return (
1094 <Suspense fallback={<Text text="Loading..." />}>
1095 <AsyncText key={tab} text={'Tab: ' + tab} />
1096 <Text key={tab + 'sibling'} text=" + sibling" />
1097 </Suspense>
1098 );
1099 }
1100
1101 ReactDOM.render(<App />, container);
1102 assertLog(['Suspend! [Tab: 0]', ' + sibling', 'Loading...']);
1103 expect(container.textContent).toEqual('Loading...');
1104
1105 await resolveText('Tab: 0');
1106 assertLog(['Tab: 0']);
1107 expect(container.textContent).toEqual('Tab: 0 + sibling');
1108
1109 await act(() => setTab(1));
1110 assertLog(['Suspend! [Tab: 1]', ' + sibling', 'Loading...']);
1111 expect(container.textContent).toEqual('Loading...');
1112
1113 await resolveText('Tab: 1');
1114 assertLog(['Tab: 1']);
1115 expect(container.textContent).toEqual('Tab: 1 + sibling');
1116
1117 await act(() => setTab(2));
1118 assertLog(['Suspend! [Tab: 2]', ' + sibling', 'Loading...']);
1119 expect(container.textContent).toEqual('Loading...');
1120
1121 await resolveText('Tab: 2');
1122 assertLog(['Tab: 2']);
1123 expect(container.textContent).toEqual('Tab: 2 + sibling');
1124 });
1125
1126 // @gate !disableLegacyMode
1127 it('does not warn if a mounted component is pinged', async () => {
1128 const {useState} = React;
1129
1130 ReactDOM.render(null, container);
1131
1132 let setStep;
1133 function UpdatingText({text, ms}) {
1134 const [step, _setStep] = useState(0);
1135 setStep = _setStep;
1136 const fullText = `${text}:${step}`;
1137 return <Text text={readText(fullText)} />;
1138 }
1139
1140 ReactDOM.render(
1141 <Suspense fallback={<Text text="Loading..." />}>
1142 <UpdatingText text="A" ms={1000} />
1143 </Suspense>,
1144 container,
1145 );
1146
1147 assertLog(['Suspend! [A:0]', 'Loading...']);
1148
1149 await resolveText('A:0');
1150 assertLog(['A:0']);
1151 expect(container.textContent).toEqual('A:0');
1152
1153 await act(() => setStep(1));
1154 assertLog(['Suspend! [A:1]', 'Loading...']);
1155 expect(container.textContent).toEqual('Loading...');
1156 });
1157
1158 // @gate !disableLegacyMode
1159 it('memoizes promise listeners per thread ID to prevent redundant renders', async () => {
1160 function App() {
1161 return (
1162 <Suspense fallback={<Text text="Loading..." />}>
1163 <AsyncText text="A" />
1164 <AsyncText text="B" />
1165 <AsyncText text="C" />
1166 </Suspense>
1167 );
1168 }
1169
1170 ReactDOM.render(null, container);
1171
1172 ReactDOM.render(<App />, container);
1173
1174 assertLog(['Suspend! [A]', 'Suspend! [B]', 'Suspend! [C]', 'Loading...']);
1175
1176 await resolveText('A');
1177 assertLog([
1178 'A',
1179 // The promises for B and C have now been thrown twice
1180 'Suspend! [B]',
1181 'Suspend! [C]',
1182 ]);
1183
1184 await resolveText('B');
1185 assertLog([
1186 // Even though the promise for B was thrown twice, we should only
1187 // re-render once.
1188 'B',
1189 // The promise for C has now been thrown three times
1190 'Suspend! [C]',
1191 ]);
1192
1193 await resolveText('C');
1194 assertLog([
1195 // Even though the promise for C was thrown three times, we should only
1196 // re-render once.
1197 'C',
1198 ]);
1199 });
1200
1201 // @gate !disableLegacyMode
1202 it('#14162', async () => {
1203 const {lazy} = React;
1204
1205 function Hello() {
1206 return <span>hello</span>;
1207 }
1208
1209 async function fetchComponent() {
1210 return new Promise(r => {
1211 // simulating a delayed import() call
1212 setTimeout(r, 1000, {default: Hello});
1213 });
1214 }
1215
1216 const LazyHello = lazy(fetchComponent);
1217
1218 class App extends React.Component {
1219 state = {render: false};
1220
1221 componentDidMount() {
1222 setTimeout(() => this.setState({render: true}));
1223 }
1224
1225 render() {
1226 return (
1227 <Suspense fallback={<span>loading...</span>}>
1228 {this.state.render && <LazyHello />}
1229 </Suspense>
1230 );
1231 }
1232 }
1233
1234 ReactDOM.render(null, container);
1235
1236 ReactDOM.render(<App name="world" />, container);
1237 });
1238
1239 // @gate !disableLegacyMode
1240 it('updates memoized child of suspense component when context updates (simple memo)', async () => {
1241 const {useContext, createContext, useState, memo} = React;
1242
1243 const ValueContext = createContext(null);
1244
1245 const MemoizedChild = memo(function MemoizedChild() {
1246 const text = useContext(ValueContext);
1247 return <Text text={readText(text)} />;
1248 });
1249
1250 let setValue;
1251 function App() {
1252 const [value, _setValue] = useState('default');
1253 setValue = _setValue;
1254
1255 return (
1256 <ValueContext.Provider value={value}>
1257 <Suspense fallback={<Text text="Loading..." />}>
1258 <MemoizedChild />
1259 </Suspense>
1260 </ValueContext.Provider>
1261 );
1262 }
1263
1264 ReactDOM.render(<App />, container);
1265 assertLog(['Suspend! [default]', 'Loading...']);
1266
1267 await resolveText('default');
1268 assertLog(['default']);
1269 expect(container.textContent).toEqual('default');
1270
1271 await act(() => setValue('new value'));
1272 assertLog(['Suspend! [new value]', 'Loading...']);
1273
1274 await resolveText('new value');
1275 assertLog(['new value']);
1276 expect(container.textContent).toEqual('new value');
1277 });
1278
1279 // @gate !disableLegacyMode
1280 it('updates memoized child of suspense component when context updates (manual memo)', async () => {
1281 const {useContext, createContext, useState, memo} = React;
1282
1283 const ValueContext = createContext(null);
1284
1285 const MemoizedChild = memo(
1286 function MemoizedChild() {
1287 const text = useContext(ValueContext);
1288 return <Text text={readText(text)} />;
1289 },
1290 function areEqual(prevProps, nextProps) {
1291 return true;
1292 },
1293 );
1294
1295 let setValue;
1296 function App() {
1297 const [value, _setValue] = useState('default');
1298 setValue = _setValue;
1299
1300 return (
1301 <ValueContext.Provider value={value}>
1302 <Suspense fallback={<Text text="Loading..." />}>
1303 <MemoizedChild />
1304 </Suspense>
1305 </ValueContext.Provider>
1306 );
1307 }
1308
1309 ReactDOM.render(<App />, container);
1310 assertLog(['Suspend! [default]', 'Loading...']);
1311
1312 await resolveText('default');
1313 assertLog(['default']);
1314 expect(container.textContent).toEqual('default');
1315
1316 await act(() => setValue('new value'));
1317 assertLog(['Suspend! [new value]', 'Loading...']);
1318
1319 await resolveText('new value');
1320 assertLog(['new value']);
1321 expect(container.textContent).toEqual('new value');
1322 });
1323
1324 // @gate !disableLegacyMode
1325 it('updates memoized child of suspense component when context updates (function)', async () => {
1326 const {useContext, createContext, useState} = React;
1327
1328 const ValueContext = createContext(null);
1329
1330 function MemoizedChild() {
1331 const text = useContext(ValueContext);
1332 return <Text text={readText(text)} />;
1333 }
1334
1335 let setValue;
1336 function App({children}) {
1337 const [value, _setValue] = useState('default');
1338 setValue = _setValue;
1339
1340 return (
1341 <ValueContext.Provider value={value}>
1342 {children}
1343 </ValueContext.Provider>
1344 );
1345 }
1346
1347 ReactDOM.render(
1348 <App>
1349 <Suspense fallback={<Text text="Loading..." />}>
1350 <MemoizedChild />
1351 </Suspense>
1352 </App>,
1353 container,
1354 );
1355 assertLog(['Suspend! [default]', 'Loading...']);
1356
1357 await resolveText('default');
1358 assertLog(['default']);
1359 expect(container.textContent).toEqual('default');
1360
1361 await act(() => setValue('new value'));
1362 assertLog(['Suspend! [new value]', 'Loading...']);
1363
1364 await resolveText('new value');
1365 assertLog(['new value']);
1366 expect(container.textContent).toEqual('new value');
1367 });
1368
1369 // @gate !disableLegacyMode
1370 it('updates memoized child of suspense component when context updates (forwardRef)', async () => {
1371 const {forwardRef, useContext, createContext, useState} = React;
1372
1373 const ValueContext = createContext(null);
1374
1375 const MemoizedChild = forwardRef(function MemoizedChild() {
1376 const text = useContext(ValueContext);
1377 return <Text text={readText(text)} />;
1378 });
1379
1380 let setValue;
1381 function App() {
1382 const [value, _setValue] = useState('default');
1383 setValue = _setValue;
1384
1385 return (
1386 <ValueContext.Provider value={value}>
1387 <Suspense fallback={<Text text="Loading..." />}>
1388 <MemoizedChild />
1389 </Suspense>
1390 </ValueContext.Provider>
1391 );
1392 }
1393
1394 ReactDOM.render(<App />, container);
1395 assertLog(['Suspend! [default]', 'Loading...']);
1396
1397 await resolveText('default');
1398 assertLog(['default']);
1399 expect(container.textContent).toEqual('default');
1400
1401 await act(() => setValue('new value'));
1402 assertLog(['Suspend! [new value]', 'Loading...']);
1403
1404 await resolveText('new value');
1405 assertLog(['new value']);
1406 expect(container.textContent).toEqual('new value');
1407 });
1408
1409 // @gate !disableLegacyMode
1410 it('updates context consumer within child of suspended suspense component when context updates', async () => {
1411 const {createContext, useState} = React;
1412
1413 const ValueContext = createContext(null);
1414
1415 const promiseThatNeverResolves = new Promise(() => {});
1416 function Child() {
1417 return (
1418 <ValueContext.Consumer>
1419 {value => {
1420 Scheduler.log(`Received context value [${value}]`);
1421 if (value === 'default') return <Text text="default" />;
1422 throw promiseThatNeverResolves;
1423 }}
1424 </ValueContext.Consumer>
1425 );
1426 }
1427
1428 let setValue;
1429 function Wrapper({children}) {
1430 const [value, _setValue] = useState('default');
1431 setValue = _setValue;
1432 return (
1433 <ValueContext.Provider value={value}>
1434 {children}
1435 </ValueContext.Provider>
1436 );
1437 }
1438
1439 function App() {
1440 return (
1441 <Wrapper>
1442 <Suspense fallback={<Text text="Loading..." />}>
1443 <Child />
1444 </Suspense>
1445 </Wrapper>
1446 );
1447 }
1448
1449 ReactDOM.render(<App />, container);
1450 assertLog(['Received context value [default]', 'default']);
1451 expect(container.textContent).toEqual('default');
1452
1453 await act(() => setValue('new value'));
1454 assertLog(['Received context value [new value]', 'Loading...']);
1455 expect(container.textContent).toEqual('Loading...');
1456
1457 await act(() => setValue('default'));
1458 assertLog(['Received context value [default]', 'default']);
1459 expect(container.textContent).toEqual('default');
1460 });
1461 });
1462 });