main
js 1,152 lines 34.3 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
8 'use strict';
9
10 let React;
11 let ReactNoop;
12 let Scheduler;
13 let act;
14 let startTransition;
15 let useDeferredValue;
16 let useMemo;
17 let useState;
18 let Suspense;
19 let Activity;
20 let assertLog;
21 let waitForPaint;
22 let textCache;
23
24 describe('ReactDeferredValue', () => {
25 beforeEach(() => {
26 jest.resetModules();
27
28 React = require('react');
29 ReactNoop = require('react-noop-renderer');
30 Scheduler = require('scheduler');
31 act = require('internal-test-utils').act;
32 startTransition = React.startTransition;
33 useDeferredValue = React.useDeferredValue;
34 useMemo = React.useMemo;
35 useState = React.useState;
36 Suspense = React.Suspense;
37 Activity = React.Activity;
38
39 const InternalTestUtils = require('internal-test-utils');
40 assertLog = InternalTestUtils.assertLog;
41 waitForPaint = InternalTestUtils.waitForPaint;
42
43 textCache = new Map();
44 });
45
46 function resolveText(text) {
47 const record = textCache.get(text);
48 if (record === undefined) {
49 const newRecord = {
50 status: 'resolved',
51 value: text,
52 };
53 textCache.set(text, newRecord);
54 } else if (record.status === 'pending') {
55 const thenable = record.value;
56 record.status = 'resolved';
57 record.value = text;
58 thenable.pings.forEach(t => t());
59 }
60 }
61
62 function readText(text) {
63 const record = textCache.get(text);
64 if (record !== undefined) {
65 switch (record.status) {
66 case 'pending':
67 Scheduler.log(`Suspend! [${text}]`);
68 throw record.value;
69 case 'rejected':
70 throw record.value;
71 case 'resolved':
72 return record.value;
73 }
74 } else {
75 Scheduler.log(`Suspend! [${text}]`);
76 const thenable = {
77 pings: [],
78 then(resolve) {
79 if (newRecord.status === 'pending') {
80 thenable.pings.push(resolve);
81 } else {
82 Promise.resolve().then(() => resolve(newRecord.value));
83 }
84 },
85 };
86
87 const newRecord = {
88 status: 'pending',
89 value: thenable,
90 };
91 textCache.set(text, newRecord);
92
93 throw thenable;
94 }
95 }
96
97 function Text({text}) {
98 Scheduler.log(text);
99 return text;
100 }
101
102 function AsyncText({text}) {
103 readText(text);
104 Scheduler.log(text);
105 return text;
106 }
107
108 it('does not cause an infinite defer loop if the original value isn\t memoized', async () => {
109 function App({value}) {
110 // The object passed to useDeferredValue is never the same as the previous
111 // render. A naive implementation would endlessly spawn deferred renders.
112 const {value: deferredValue} = useDeferredValue({value});
113
114 const child = useMemo(
115 () => <Text text={'Original: ' + value} />,
116 [value],
117 );
118
119 const deferredChild = useMemo(
120 () => <Text text={'Deferred: ' + deferredValue} />,
121 [deferredValue],
122 );
123
124 return (
125 <div>
126 <div>{child}</div>
127 <div>{deferredChild}</div>
128 </div>
129 );
130 }
131
132 const root = ReactNoop.createRoot();
133
134 // Initial render
135 await act(() => {
136 root.render(<App value={1} />);
137 });
138 assertLog(['Original: 1', 'Deferred: 1']);
139
140 // If it's an urgent update, the value is deferred
141 await act(async () => {
142 root.render(<App value={2} />);
143
144 await waitForPaint(['Original: 2']);
145 // The deferred value updates in a separate render
146 await waitForPaint(['Deferred: 2']);
147 });
148 expect(root).toMatchRenderedOutput(
149 <div>
150 <div>Original: 2</div>
151 <div>Deferred: 2</div>
152 </div>,
153 );
154
155 // But if it updates during a transition, it doesn't defer
156 await act(async () => {
157 startTransition(() => {
158 root.render(<App value={3} />);
159 });
160 // The deferred value updates in the same render as the original
161 await waitForPaint(['Original: 3', 'Deferred: 3']);
162 });
163 expect(root).toMatchRenderedOutput(
164 <div>
165 <div>Original: 3</div>
166 <div>Deferred: 3</div>
167 </div>,
168 );
169 });
170
171 it('does not defer during a transition', async () => {
172 function App({value}) {
173 const deferredValue = useDeferredValue(value);
174
175 const child = useMemo(
176 () => <Text text={'Original: ' + value} />,
177 [value],
178 );
179
180 const deferredChild = useMemo(
181 () => <Text text={'Deferred: ' + deferredValue} />,
182 [deferredValue],
183 );
184
185 return (
186 <div>
187 <div>{child}</div>
188 <div>{deferredChild}</div>
189 </div>
190 );
191 }
192
193 const root = ReactNoop.createRoot();
194
195 // Initial render
196 await act(() => {
197 root.render(<App value={1} />);
198 });
199 assertLog(['Original: 1', 'Deferred: 1']);
200
201 // If it's an urgent update, the value is deferred
202 await act(async () => {
203 root.render(<App value={2} />);
204
205 await waitForPaint(['Original: 2']);
206 // The deferred value updates in a separate render
207 await waitForPaint(['Deferred: 2']);
208 });
209 expect(root).toMatchRenderedOutput(
210 <div>
211 <div>Original: 2</div>
212 <div>Deferred: 2</div>
213 </div>,
214 );
215
216 // But if it updates during a transition, it doesn't defer
217 await act(async () => {
218 startTransition(() => {
219 root.render(<App value={3} />);
220 });
221 // The deferred value updates in the same render as the original
222 await waitForPaint(['Original: 3', 'Deferred: 3']);
223 });
224 expect(root).toMatchRenderedOutput(
225 <div>
226 <div>Original: 3</div>
227 <div>Deferred: 3</div>
228 </div>,
229 );
230 });
231
232 it("works if there's a render phase update", async () => {
233 function App({value: propValue}) {
234 const [value, setValue] = useState(null);
235 if (value !== propValue) {
236 setValue(propValue);
237 }
238
239 const deferredValue = useDeferredValue(value);
240
241 const child = useMemo(
242 () => <Text text={'Original: ' + value} />,
243 [value],
244 );
245
246 const deferredChild = useMemo(
247 () => <Text text={'Deferred: ' + deferredValue} />,
248 [deferredValue],
249 );
250
251 return (
252 <div>
253 <div>{child}</div>
254 <div>{deferredChild}</div>
255 </div>
256 );
257 }
258
259 const root = ReactNoop.createRoot();
260
261 // Initial render
262 await act(() => {
263 root.render(<App value={1} />);
264 });
265 assertLog(['Original: 1', 'Deferred: 1']);
266
267 // If it's an urgent update, the value is deferred
268 await act(async () => {
269 root.render(<App value={2} />);
270
271 await waitForPaint(['Original: 2']);
272 // The deferred value updates in a separate render
273 await waitForPaint(['Deferred: 2']);
274 });
275 expect(root).toMatchRenderedOutput(
276 <div>
277 <div>Original: 2</div>
278 <div>Deferred: 2</div>
279 </div>,
280 );
281
282 // But if it updates during a transition, it doesn't defer
283 await act(async () => {
284 startTransition(() => {
285 root.render(<App value={3} />);
286 });
287 // The deferred value updates in the same render as the original
288 await waitForPaint(['Original: 3', 'Deferred: 3']);
289 });
290 expect(root).toMatchRenderedOutput(
291 <div>
292 <div>Original: 3</div>
293 <div>Deferred: 3</div>
294 </div>,
295 );
296 });
297
298 it('regression test: during urgent update, reuse previous value, not initial value', async () => {
299 function App({value: propValue}) {
300 const [value, setValue] = useState(null);
301 if (value !== propValue) {
302 setValue(propValue);
303 }
304
305 const deferredValue = useDeferredValue(value);
306
307 const child = useMemo(
308 () => <Text text={'Original: ' + value} />,
309 [value],
310 );
311
312 const deferredChild = useMemo(
313 () => <Text text={'Deferred: ' + deferredValue} />,
314 [deferredValue],
315 );
316
317 return (
318 <div>
319 <div>{child}</div>
320 <div>{deferredChild}</div>
321 </div>
322 );
323 }
324
325 const root = ReactNoop.createRoot();
326
327 // Initial render
328 await act(async () => {
329 root.render(<App value={1} />);
330 await waitForPaint(['Original: 1', 'Deferred: 1']);
331 expect(root).toMatchRenderedOutput(
332 <div>
333 <div>Original: 1</div>
334 <div>Deferred: 1</div>
335 </div>,
336 );
337 });
338
339 await act(async () => {
340 startTransition(() => {
341 root.render(<App value={2} />);
342 });
343 // In the regression, the memoized value was not updated during non-urgent
344 // updates, so this would flip the deferred value back to the initial
345 // value (1) instead of reusing the current one (2).
346 await waitForPaint(['Original: 2', 'Deferred: 2']);
347 expect(root).toMatchRenderedOutput(
348 <div>
349 <div>Original: 2</div>
350 <div>Deferred: 2</div>
351 </div>,
352 );
353 });
354
355 await act(async () => {
356 root.render(<App value={3} />);
357 await waitForPaint(['Original: 3']);
358 expect(root).toMatchRenderedOutput(
359 <div>
360 <div>Original: 3</div>
361 <div>Deferred: 2</div>
362 </div>,
363 );
364 await waitForPaint(['Deferred: 3']);
365 expect(root).toMatchRenderedOutput(
366 <div>
367 <div>Original: 3</div>
368 <div>Deferred: 3</div>
369 </div>,
370 );
371 });
372 });
373
374 it('supports initialValue argument', async () => {
375 function App() {
376 const value = useDeferredValue('Final', 'Initial');
377 return <Text text={value} />;
378 }
379
380 const root = ReactNoop.createRoot();
381 await act(async () => {
382 root.render(<App />);
383 await waitForPaint(['Initial']);
384 expect(root).toMatchRenderedOutput('Initial');
385 });
386 assertLog(['Final']);
387 expect(root).toMatchRenderedOutput('Final');
388 });
389
390 it('defers during initial render when initialValue is provided, even if render is not sync', async () => {
391 function App() {
392 const value = useDeferredValue('Final', 'Initial');
393 return <Text text={value} />;
394 }
395
396 const root = ReactNoop.createRoot();
397 await act(async () => {
398 // Initial mount is a transition, but it should defer anyway
399 startTransition(() => root.render(<App />));
400 await waitForPaint(['Initial']);
401 expect(root).toMatchRenderedOutput('Initial');
402 });
403 assertLog(['Final']);
404 expect(root).toMatchRenderedOutput('Final');
405 });
406
407 it(
408 'if a suspended render spawns a deferred task, we can switch to the ' +
409 'deferred task without finishing the original one (no Suspense boundary)',
410 async () => {
411 function App() {
412 const text = useDeferredValue('Final', 'Loading...');
413 return <AsyncText text={text} />;
414 }
415
416 const root = ReactNoop.createRoot();
417 await act(() => root.render(<App />));
418 assertLog([
419 'Suspend! [Loading...]',
420 // The initial value suspended, so we attempt the final value, which
421 // also suspends.
422 'Suspend! [Final]',
423 ...(gate('enableParallelTransitions')
424 ? []
425 : [
426 // Existing bug: Unnecessary pre-warm.
427 'Suspend! [Loading...]',
428 'Suspend! [Final]',
429 ]),
430 ]);
431 expect(root).toMatchRenderedOutput(null);
432
433 // The final value loads, so we can skip the initial value entirely.
434 await act(() => resolveText('Final'));
435 assertLog(['Final']);
436 expect(root).toMatchRenderedOutput('Final');
437
438 // When the initial value finally loads, nothing happens because we no
439 // longer need it.
440 await act(() => resolveText('Loading...'));
441 assertLog([]);
442 expect(root).toMatchRenderedOutput('Final');
443 },
444 );
445
446 it(
447 'if a suspended render spawns a deferred task that suspends on a sibling, ' +
448 'we can finish the original task if the original sibling loads first',
449 async () => {
450 function App() {
451 const deferredText = useDeferredValue(`Final`, `Loading...`);
452 return (
453 <>
454 <AsyncText text={deferredText} />{' '}
455 <AsyncText text={`Sibling: ${deferredText}`} />
456 </>
457 );
458 }
459
460 const root = ReactNoop.createRoot();
461 await act(() => root.render(<App text="a" />));
462 assertLog([
463 'Suspend! [Loading...]',
464 // The initial value suspended, so we attempt the final value, which
465 // also suspends.
466 'Suspend! [Final]',
467 'Suspend! [Sibling: Final]',
468 ...(gate('enableParallelTransitions')
469 ? [
470 // With parallel transitions,
471 // we do not continue pre-warming.
472 ]
473 : [
474 'Suspend! [Loading...]',
475 'Suspend! [Sibling: Loading...]',
476 'Suspend! [Final]',
477 'Suspend! [Sibling: Final]',
478 ]),
479 ]);
480 expect(root).toMatchRenderedOutput(null);
481
482 // The final value loads, so we can skip the initial value entirely.
483 await act(() => {
484 resolveText('Final');
485 });
486 assertLog(['Final', 'Suspend! [Sibling: Final]']);
487 expect(root).toMatchRenderedOutput(null);
488
489 // The initial value resolves first, so we render that.
490 await act(() => resolveText('Loading...'));
491 assertLog([
492 'Loading...',
493 'Suspend! [Sibling: Loading...]',
494 'Final',
495 'Suspend! [Sibling: Final]',
496 ...(gate('enableParallelTransitions')
497 ? [
498 // With parallel transitions,
499 // we do not continue pre-warming.
500 ]
501 : [
502 'Loading...',
503 'Suspend! [Sibling: Loading...]',
504 'Final',
505 'Suspend! [Sibling: Final]',
506 ]),
507 ]);
508 expect(root).toMatchRenderedOutput(null);
509
510 // The Final sibling loads, we're unblocked and commit.
511 await act(() => {
512 resolveText('Sibling: Final');
513 });
514 assertLog(['Final', 'Sibling: Final']);
515 expect(root).toMatchRenderedOutput('Final Sibling: Final');
516
517 // We already rendered the Final value, so nothing happens
518 await act(() => {
519 resolveText('Sibling: Loading...');
520 });
521 assertLog([]);
522 expect(root).toMatchRenderedOutput('Final Sibling: Final');
523 },
524 );
525
526 it(
527 'if a suspended render spawns a deferred task that suspends on a sibling,' +
528 ' we can switch to the deferred task without finishing the original one',
529 async () => {
530 function App() {
531 const deferredText = useDeferredValue(`Final`, `Loading...`);
532 return (
533 <>
534 <AsyncText text={deferredText} />{' '}
535 <AsyncText text={`Sibling: ${deferredText}`} />
536 </>
537 );
538 }
539
540 const root = ReactNoop.createRoot();
541 await act(() => root.render(<App text="a" />));
542 assertLog([
543 'Suspend! [Loading...]',
544 // The initial value suspended, so we attempt the final value, which
545 // also suspends.
546 'Suspend! [Final]',
547 'Suspend! [Sibling: Final]',
548 ...(gate('enableParallelTransitions')
549 ? [
550 // With parallel transitions,
551 // we do not continue pre-warming.
552 ]
553 : [
554 'Suspend! [Loading...]',
555 'Suspend! [Sibling: Loading...]',
556 'Suspend! [Final]',
557 'Suspend! [Sibling: Final]',
558 ]),
559 ]);
560 expect(root).toMatchRenderedOutput(null);
561
562 // The final value loads, so we can skip the initial value entirely.
563 await act(() => {
564 resolveText('Final');
565 });
566 assertLog(['Final', 'Suspend! [Sibling: Final]']);
567 expect(root).toMatchRenderedOutput(null);
568
569 // The initial value resolves first, so we render that.
570 await act(() => resolveText('Loading...'));
571 assertLog([
572 'Loading...',
573 'Suspend! [Sibling: Loading...]',
574 'Final',
575 'Suspend! [Sibling: Final]',
576 ...(gate('enableParallelTransitions')
577 ? [
578 // With parallel transitions,
579 // we do not continue pre-warming.
580 ]
581 : [
582 'Loading...',
583 'Suspend! [Sibling: Loading...]',
584 'Final',
585 'Suspend! [Sibling: Final]',
586 ]),
587 ]);
588 expect(root).toMatchRenderedOutput(null);
589
590 // The initial sibling loads, we're unblocked and commit.
591 await act(() => {
592 resolveText('Sibling: Loading...');
593 });
594 assertLog([
595 'Loading...',
596 'Sibling: Loading...',
597 'Final',
598 'Suspend! [Sibling: Final]',
599 ]);
600 expect(root).toMatchRenderedOutput('Loading... Sibling: Loading...');
601
602 // Now unblock the final sibling.
603 await act(() => {
604 resolveText('Sibling: Final');
605 });
606 assertLog(['Final', 'Sibling: Final']);
607 expect(root).toMatchRenderedOutput('Final Sibling: Final');
608 },
609 );
610
611 it(
612 'if a suspended render spawns a deferred task, we can switch to the ' +
613 'deferred task without finishing the original one (no Suspense boundary, ' +
614 'synchronous parent update)',
615 async () => {
616 function App() {
617 const text = useDeferredValue('Final', 'Loading...');
618 return <AsyncText text={text} />;
619 }
620
621 const root = ReactNoop.createRoot();
622 // TODO: This made me realize that we don't warn if an update spawns a
623 // deferred task without being wrapped with `act`. Usually it would work
624 // anyway because the parent task has to wrapped with `act`... but not
625 // if it was flushed with `flushSync` instead.
626 await act(() => {
627 ReactNoop.flushSync(() => root.render(<App />));
628 });
629 assertLog([
630 'Suspend! [Loading...]',
631 // The initial value suspended, so we attempt the final value, which
632 // also suspends.
633 'Suspend! [Final]',
634 ...(gate('enableParallelTransitions')
635 ? [
636 // With parallel transitions,
637 // we do not continue pre-warming.
638 ]
639 : ['Suspend! [Loading...]', 'Suspend! [Final]']),
640 ]);
641 expect(root).toMatchRenderedOutput(null);
642
643 // The final value loads, so we can skip the initial value entirely.
644 await act(() => resolveText('Final'));
645 assertLog(['Final']);
646 expect(root).toMatchRenderedOutput('Final');
647
648 // When the initial value finally loads, nothing happens because we no
649 // longer need it.
650 await act(() => resolveText('Loading...'));
651 assertLog([]);
652 expect(root).toMatchRenderedOutput('Final');
653 },
654 );
655
656 it(
657 'if a suspended render spawns a deferred task, we can switch to the ' +
658 'deferred task without finishing the original one (Suspense boundary)',
659 async () => {
660 function App() {
661 const text = useDeferredValue('Final', 'Loading...');
662 return <AsyncText text={text} />;
663 }
664
665 const root = ReactNoop.createRoot();
666 await act(() =>
667 root.render(
668 <Suspense fallback={<Text text="Fallback" />}>
669 <App />
670 </Suspense>,
671 ),
672 );
673 assertLog([
674 'Suspend! [Loading...]',
675 'Fallback',
676
677 // The initial value suspended, so we attempt the final value, which
678 // also suspends.
679 'Suspend! [Final]',
680 // pre-warming
681 'Suspend! [Final]',
682 ]);
683 expect(root).toMatchRenderedOutput('Fallback');
684
685 // The final value loads, so we can skip the initial value entirely.
686 await act(() => resolveText('Final'));
687 assertLog(['Final']);
688 expect(root).toMatchRenderedOutput('Final');
689
690 // When the initial value finally loads, nothing happens because we no
691 // longer need it.
692 await act(() => resolveText('Loading...'));
693 assertLog([]);
694 expect(root).toMatchRenderedOutput('Final');
695 },
696 );
697
698 it(
699 'if a suspended render spawns a deferred task that also suspends, we can ' +
700 'finish the original task if that one loads first',
701 async () => {
702 function App() {
703 const text = useDeferredValue('Final', 'Loading...');
704 return <AsyncText text={text} />;
705 }
706
707 const root = ReactNoop.createRoot();
708 await act(() => root.render(<App />));
709 assertLog([
710 'Suspend! [Loading...]',
711 // The initial value suspended, so we attempt the final value, which
712 // also suspends.
713 'Suspend! [Final]',
714 ...(gate('enableParallelTransitions')
715 ? [
716 // With parallel transitions,
717 // we do not continue pre-warming.
718 ]
719 : ['Suspend! [Loading...]', 'Suspend! [Final]']),
720 ]);
721 expect(root).toMatchRenderedOutput(null);
722
723 // The initial value resolves first, so we render that.
724 await act(() => resolveText('Loading...'));
725 assertLog([
726 'Loading...',
727 // Still waiting for the final value.
728 'Suspend! [Final]',
729 ]);
730 expect(root).toMatchRenderedOutput('Loading...');
731
732 // The final value loads, so we can switch to that.
733 await act(() => resolveText('Final'));
734 assertLog(['Final']);
735 expect(root).toMatchRenderedOutput('Final');
736 },
737 );
738
739 it(
740 'if there are multiple useDeferredValues in the same tree, only the ' +
741 'first level defers; subsequent ones go straight to the final value, to ' +
742 'avoid a waterfall',
743 async () => {
744 function App() {
745 const showContent = useDeferredValue(true, false);
746 if (!showContent) {
747 return <Text text="App Preview" />;
748 }
749 return <Content />;
750 }
751
752 function Content() {
753 const text = useDeferredValue('Content', 'Content Preview');
754 return <AsyncText text={text} />;
755 }
756
757 const root = ReactNoop.createRoot();
758 resolveText('App Preview');
759
760 await act(() => root.render(<App />));
761 assertLog([
762 // The App shows an immediate preview
763 'App Preview',
764 // Then we switch to showing the content. The Content component also
765 // contains a useDeferredValue, but since we already showed a preview
766 // in a parent component, we skip the preview in the inner one and
767 // go straight to attempting the final value.
768 //
769 // (Note that this is intentionally different from how nested Suspense
770 // boundaries work, where we always prefer to show the innermost
771 // loading state.)
772 'Suspend! [Content]',
773 ]);
774 // Still showing the App preview state because the inner
775 // content suspended.
776 expect(root).toMatchRenderedOutput('App Preview');
777
778 // Finish loading the content
779 await act(() => resolveText('Content'));
780 // We didn't even attempt to render Content Preview.
781 assertLog(['Content']);
782 expect(root).toMatchRenderedOutput('Content');
783 },
784 );
785
786 it(
787 "regression: useDeferredValue's initial value argument works even if an unrelated " +
788 'transition is suspended',
789 async () => {
790 // Simulates a previous bug where a new useDeferredValue hook is mounted
791 // while some unrelated transition is suspended. In the regression case,
792 // the initial values was skipped/ignored.
793
794 function Content({text}) {
795 return (
796 <AsyncText text={useDeferredValue(text, `Preview ${text}...`)} />
797 );
798 }
799
800 function App({text}) {
801 // Use a key to force a new Content instance to be mounted each time
802 // the text changes.
803 return <Content key={text} text={text} />;
804 }
805
806 const root = ReactNoop.createRoot();
807
808 // Render a previous UI using useDeferredValue. Suspend on the
809 // final value.
810 resolveText('Preview A...');
811 await act(() => startTransition(() => root.render(<App text="A" />)));
812 assertLog(['Preview A...', 'Suspend! [A]']);
813
814 // While it's still suspended, update the UI to show a different screen
815 // with a different preview value. We should be able to show the new
816 // preview even though the previous transition never finished.
817 resolveText('Preview B...');
818 await act(() => startTransition(() => root.render(<App text="B" />)));
819 assertLog(['Preview B...', 'Suspend! [B]']);
820
821 // Now finish loading the final value.
822 await act(() => resolveText('B'));
823 assertLog(['B']);
824 expect(root).toMatchRenderedOutput('B');
825 },
826 );
827
828 it('avoids a useDeferredValue waterfall when separated by a Suspense boundary', async () => {
829 // Same as the previous test but with a Suspense boundary separating the
830 // two useDeferredValue hooks.
831 function App() {
832 const showContent = useDeferredValue(true, false);
833 if (!showContent) {
834 return <Text text="App Preview" />;
835 }
836 return (
837 <Suspense fallback={<Text text="Loading..." />}>
838 <Content />
839 </Suspense>
840 );
841 }
842
843 function Content() {
844 const text = useDeferredValue('Content', 'Content Preview');
845 return <AsyncText text={text} />;
846 }
847
848 const root = ReactNoop.createRoot();
849 resolveText('App Preview');
850
851 await act(() => root.render(<App />));
852 assertLog([
853 // The App shows an immediate preview
854 'App Preview',
855 // Then we switch to showing the content. The Content component also
856 // contains a useDeferredValue, but since we already showed a preview
857 // in a parent component, we skip the preview in the inner one and
858 // go straight to attempting the final value.
859 'Suspend! [Content]',
860 'Loading...',
861 // pre-warming
862 'Suspend! [Content]',
863 ]);
864 // The content suspended, so we show a Suspense fallback
865 expect(root).toMatchRenderedOutput('Loading...');
866
867 // Finish loading the content
868 await act(() => resolveText('Content'));
869 // We didn't even attempt to render Content Preview.
870 assertLog(['Content']);
871 expect(root).toMatchRenderedOutput('Content');
872 });
873
874 it('useDeferredValue can spawn a deferred task while prerendering a hidden tree', async () => {
875 function App() {
876 const text = useDeferredValue('Final', 'Preview');
877 return (
878 <div>
879 <AsyncText text={text} />
880 </div>
881 );
882 }
883
884 let revealContent;
885 function Container({children}) {
886 const [shouldShow, setState] = useState(false);
887 revealContent = () => setState(true);
888 return (
889 <Activity mode={shouldShow ? 'visible' : 'hidden'}>{children}</Activity>
890 );
891 }
892
893 const root = ReactNoop.createRoot();
894
895 // Prerender a hidden tree
896 resolveText('Preview');
897 await act(() =>
898 root.render(
899 <Container>
900 <App />
901 </Container>,
902 ),
903 );
904 assertLog(['Preview', 'Suspend! [Final]']);
905 expect(root).toMatchRenderedOutput(<div hidden={true}>Preview</div>);
906
907 // Finish loading the content
908 await act(() => resolveText('Final'));
909 assertLog(['Final']);
910 expect(root).toMatchRenderedOutput(<div hidden={true}>Final</div>);
911
912 // Now reveal the hidden tree. It should toggle the visibility without
913 // having to re-render anything inside the prerendered tree.
914 await act(() => revealContent());
915 assertLog([]);
916 expect(root).toMatchRenderedOutput(<div>Final</div>);
917 });
918
919 it('useDeferredValue can prerender the initial value inside a hidden tree', async () => {
920 function App({text}) {
921 const renderedText = useDeferredValue(text, `Preview [${text}]`);
922 return (
923 <div>
924 <Text text={renderedText} />
925 </div>
926 );
927 }
928
929 let revealContent;
930 function Container({children}) {
931 const [shouldShow, setState] = useState(false);
932 revealContent = () => setState(true);
933 return (
934 <Activity mode={shouldShow ? 'visible' : 'hidden'}>{children}</Activity>
935 );
936 }
937
938 const root = ReactNoop.createRoot();
939
940 // Prerender some content
941 await act(() => {
942 root.render(
943 <Container>
944 <App text="A" />
945 </Container>,
946 );
947 });
948 assertLog(['Preview [A]', 'A']);
949 expect(root).toMatchRenderedOutput(<div hidden={true}>A</div>);
950
951 await act(async () => {
952 // While the tree is still hidden, update the pre-rendered tree.
953 root.render(
954 <Container>
955 <App text="B" />
956 </Container>,
957 );
958 // We should switch to pre-rendering the new preview.
959 await waitForPaint([]);
960 await waitForPaint(['Preview [B]']);
961 expect(root).toMatchRenderedOutput(<div hidden={true}>Preview [B]</div>);
962
963 // Before the prerender is complete, reveal the hidden tree. Because we
964 // consider revealing a hidden tree to be the same as mounting a new one,
965 // we should not skip the preview state.
966 revealContent();
967 // Because the preview state was already prerendered, we can reveal it
968 // without any addditional work.
969 if (gate(flags => flags.enableYieldingBeforePassive)) {
970 // Passive effects.
971 await waitForPaint([]);
972 }
973 await waitForPaint([]);
974 expect(root).toMatchRenderedOutput(<div>Preview [B]</div>);
975 });
976 // Finally, finish rendering the final value.
977 assertLog(['B']);
978 expect(root).toMatchRenderedOutput(<div>B</div>);
979 });
980
981 it(
982 'useDeferredValue skips the preview state when revealing a hidden tree ' +
983 'if the final value is referentially identical',
984 async () => {
985 function App({text}) {
986 const renderedText = useDeferredValue(text, `Preview [${text}]`);
987 return (
988 <div>
989 <Text text={renderedText} />
990 </div>
991 );
992 }
993
994 function Container({text, shouldShow}) {
995 return (
996 <Activity mode={shouldShow ? 'visible' : 'hidden'}>
997 <App text={text} />
998 </Activity>
999 );
1000 }
1001
1002 const root = ReactNoop.createRoot();
1003
1004 // Prerender some content
1005 await act(() => root.render(<Container text="A" shouldShow={false} />));
1006 assertLog(['Preview [A]', 'A']);
1007 expect(root).toMatchRenderedOutput(<div hidden={true}>A</div>);
1008
1009 // Reveal the prerendered tree. Because the final value is referentially
1010 // equal to what was already prerendered, we can skip the preview state
1011 // and go straight to the final one. The practical upshot of this is
1012 // that we can completely prerender the final value without having to
1013 // do additional rendering work when the tree is revealed.
1014 await act(() => root.render(<Container text="A" shouldShow={true} />));
1015 assertLog(['A']);
1016 expect(root).toMatchRenderedOutput(<div>A</div>);
1017 },
1018 );
1019
1020 it(
1021 'useDeferredValue does not skip the preview state when revealing a ' +
1022 'hidden tree if the final value is different from the currently rendered one',
1023 async () => {
1024 function App({text}) {
1025 const renderedText = useDeferredValue(text, `Preview [${text}]`);
1026 return (
1027 <div>
1028 <Text text={renderedText} />
1029 </div>
1030 );
1031 }
1032
1033 function Container({text, shouldShow}) {
1034 return (
1035 <Activity mode={shouldShow ? 'visible' : 'hidden'}>
1036 <App text={text} />
1037 </Activity>
1038 );
1039 }
1040
1041 const root = ReactNoop.createRoot();
1042
1043 // Prerender some content
1044 await act(() => root.render(<Container text="A" shouldShow={false} />));
1045 assertLog(['Preview [A]', 'A']);
1046 expect(root).toMatchRenderedOutput(<div hidden={true}>A</div>);
1047
1048 // Reveal the prerendered tree. Because the final value is different from
1049 // what was already prerendered, we can't bail out. Since we treat
1050 // revealing a hidden tree the same as a new mount, show the preview state
1051 // before switching to the final one.
1052 await act(async () => {
1053 root.render(<Container text="B" shouldShow={true} />);
1054 // First commit the preview state
1055 await waitForPaint(['Preview [B]']);
1056 expect(root).toMatchRenderedOutput(<div>Preview [B]</div>);
1057 });
1058 // Then switch to the final state
1059 assertLog(['B']);
1060 expect(root).toMatchRenderedOutput(<div>B</div>);
1061 },
1062 );
1063
1064 it(
1065 'useDeferredValue does not show "previous" value when revealing a hidden ' +
1066 'tree (no initial value)',
1067 async () => {
1068 function App({text}) {
1069 const renderedText = useDeferredValue(text);
1070 return (
1071 <div>
1072 <Text text={renderedText} />
1073 </div>
1074 );
1075 }
1076
1077 function Container({text, shouldShow}) {
1078 return (
1079 <Activity mode={shouldShow ? 'visible' : 'hidden'}>
1080 <App text={text} />
1081 </Activity>
1082 );
1083 }
1084
1085 const root = ReactNoop.createRoot();
1086
1087 // Prerender some content
1088 await act(() => root.render(<Container text="A" shouldShow={false} />));
1089 assertLog(['A']);
1090 expect(root).toMatchRenderedOutput(<div hidden={true}>A</div>);
1091
1092 // Update the prerendered tree and reveal it at the same time. Even though
1093 // this is a sync update, we should update B immediately rather than stay
1094 // on the old value (A), because conceptually this is a new tree.
1095 await act(() => root.render(<Container text="B" shouldShow={true} />));
1096 assertLog(['B']);
1097 expect(root).toMatchRenderedOutput(<div>B</div>);
1098 },
1099 );
1100
1101 // Regression test for https://github.com/facebook/react/issues/35821
1102 it('deferred value catches up when a suspension is resolved during the same render', async () => {
1103 let setValue;
1104 function App() {
1105 const [value, _setValue] = useState('initial');
1106 setValue = _setValue;
1107 const deferred = useDeferredValue(value);
1108 return (
1109 <Suspense fallback={<Text text="Loading..." />}>
1110 <AsyncText text={'A:' + deferred} />
1111 <Sibling text={deferred} />
1112 </Suspense>
1113 );
1114 }
1115
1116 function Sibling({text}) {
1117 if (text !== 'initial') {
1118 // Resolve A during this render, simulating data arriving while
1119 // a render is already in progress.
1120 resolveText('A:' + text);
1121 }
1122 readText('B:' + text);
1123 Scheduler.log('B: ' + text);
1124 return text;
1125 }
1126
1127 const root = ReactNoop.createRoot();
1128
1129 resolveText('A:initial');
1130 resolveText('B:initial');
1131 await act(() => root.render(<App />));
1132 assertLog(['A:initial', 'B: initial']);
1133
1134 // Pre-resolve B so the sibling won't suspend on retry.
1135 resolveText('B:updated');
1136
1137 await act(() => setValue('updated'));
1138 assertLog([
1139 // Sync render defers the value.
1140 'A:initial',
1141 'B: initial',
1142 // Deferred render: A suspends, then Sibling resolves A mid-render.
1143 'Suspend! [A:updated]',
1144 'B: updated',
1145 'Loading...',
1146 // React retries and the deferred value catches up.
1147 'A:updated',
1148 'B: updated',
1149 ]);
1150 expect(root).toMatchRenderedOutput('A:updatedupdated');
1151 });
1152 });