@@ -616,4 +616,190 @@ describe('ReactDeferredValue', () => {
616
616
assertLog ( [ ] ) ;
617
617
expect ( root ) . toMatchRenderedOutput ( < div > Final</ div > ) ;
618
618
} ) ;
619
+
620
+ // @gate enableUseDeferredValueInitialArg
621
+ // @gate enableOffscreen
622
+ it ( 'useDeferredValue can prerender the initial value inside a hidden tree' , async ( ) => {
623
+ function App ( { text} ) {
624
+ const renderedText = useDeferredValue ( text , `Preview [${ text } ]` ) ;
625
+ return (
626
+ < div >
627
+ < Text text = { renderedText } />
628
+ </ div >
629
+ ) ;
630
+ }
631
+
632
+ let revealContent ;
633
+ function Container ( { children} ) {
634
+ const [ shouldShow , setState ] = useState ( false ) ;
635
+ revealContent = ( ) => setState ( true ) ;
636
+ return (
637
+ < Offscreen mode = { shouldShow ? 'visible' : 'hidden' } >
638
+ { children }
639
+ </ Offscreen >
640
+ ) ;
641
+ }
642
+
643
+ const root = ReactNoop . createRoot ( ) ;
644
+
645
+ // Prerender some content
646
+ await act ( ( ) => {
647
+ root . render (
648
+ < Container >
649
+ < App text = "A" />
650
+ </ Container > ,
651
+ ) ;
652
+ } ) ;
653
+ assertLog ( [ 'Preview [A]' , 'A' ] ) ;
654
+ expect ( root ) . toMatchRenderedOutput ( < div hidden = { true } > A</ div > ) ;
655
+
656
+ await act ( async ( ) => {
657
+ // While the tree is still hidden, update the pre-rendered tree.
658
+ root . render (
659
+ < Container >
660
+ < App text = "B" />
661
+ </ Container > ,
662
+ ) ;
663
+ // We should switch to pre-rendering the new preview.
664
+ await waitForPaint ( [ 'Preview [B]' ] ) ;
665
+ expect ( root ) . toMatchRenderedOutput ( < div hidden = { true } > Preview [B]</ div > ) ;
666
+
667
+ // Before the prerender is complete, reveal the hidden tree. Because we
668
+ // consider revealing a hidden tree to be the same as mounting a new one,
669
+ // we should not skip the preview state.
670
+ revealContent ( ) ;
671
+ // Because the preview state was already prerendered, we can reveal it
672
+ // without any addditional work.
673
+ await waitForPaint ( [ ] ) ;
674
+ expect ( root ) . toMatchRenderedOutput ( < div > Preview [B]</ div > ) ;
675
+ } ) ;
676
+ // Finally, finish rendering the final value.
677
+ assertLog ( [ 'B' ] ) ;
678
+ expect ( root ) . toMatchRenderedOutput ( < div > B</ div > ) ;
679
+ } ) ;
680
+
681
+ // @gate enableUseDeferredValueInitialArg
682
+ // @gate enableOffscreen
683
+ it (
684
+ 'useDeferredValue skips the preview state when revealing a hidden tree ' +
685
+ 'if the final value is referentially identical' ,
686
+ async ( ) => {
687
+ function App ( { text} ) {
688
+ const renderedText = useDeferredValue ( text , `Preview [${ text } ]` ) ;
689
+ return (
690
+ < div >
691
+ < Text text = { renderedText } />
692
+ </ div >
693
+ ) ;
694
+ }
695
+
696
+ function Container ( { text, shouldShow} ) {
697
+ return (
698
+ < Offscreen mode = { shouldShow ? 'visible' : 'hidden' } >
699
+ < App text = { text } />
700
+ </ Offscreen >
701
+ ) ;
702
+ }
703
+
704
+ const root = ReactNoop . createRoot ( ) ;
705
+
706
+ // Prerender some content
707
+ await act ( ( ) => root . render ( < Container text = "A" shouldShow = { false } /> ) ) ;
708
+ assertLog ( [ 'Preview [A]' , 'A' ] ) ;
709
+ expect ( root ) . toMatchRenderedOutput ( < div hidden = { true } > A</ div > ) ;
710
+
711
+ // Reveal the prerendered tree. Because the final value is referentially
712
+ // equal to what was already prerendered, we can skip the preview state
713
+ // and go straight to the final one. The practical upshot of this is
714
+ // that we can completely prerender the final value without having to
715
+ // do additional rendering work when the tree is revealed.
716
+ await act ( ( ) => root . render ( < Container text = "A" shouldShow = { true } /> ) ) ;
717
+ assertLog ( [ 'A' ] ) ;
718
+ expect ( root ) . toMatchRenderedOutput ( < div > A</ div > ) ;
719
+ } ,
720
+ ) ;
721
+
722
+ // @gate enableUseDeferredValueInitialArg
723
+ // @gate enableOffscreen
724
+ it (
725
+ 'useDeferredValue does not skip the preview state when revealing a ' +
726
+ 'hidden tree if the final value is different from the currently rendered one' ,
727
+ async ( ) => {
728
+ function App ( { text} ) {
729
+ const renderedText = useDeferredValue ( text , `Preview [${ text } ]` ) ;
730
+ return (
731
+ < div >
732
+ < Text text = { renderedText } />
733
+ </ div >
734
+ ) ;
735
+ }
736
+
737
+ function Container ( { text, shouldShow} ) {
738
+ return (
739
+ < Offscreen mode = { shouldShow ? 'visible' : 'hidden' } >
740
+ < App text = { text } />
741
+ </ Offscreen >
742
+ ) ;
743
+ }
744
+
745
+ const root = ReactNoop . createRoot ( ) ;
746
+
747
+ // Prerender some content
748
+ await act ( ( ) => root . render ( < Container text = "A" shouldShow = { false } /> ) ) ;
749
+ assertLog ( [ 'Preview [A]' , 'A' ] ) ;
750
+ expect ( root ) . toMatchRenderedOutput ( < div hidden = { true } > A</ div > ) ;
751
+
752
+ // Reveal the prerendered tree. Because the final value is different from
753
+ // what was already prerendered, we can't bail out. Since we treat
754
+ // revealing a hidden tree the same as a new mount, show the preview state
755
+ // before switching to the final one.
756
+ await act ( async ( ) => {
757
+ root . render ( < Container text = "B" shouldShow = { true } /> ) ;
758
+ // First commit the preview state
759
+ await waitForPaint ( [ 'Preview [B]' ] ) ;
760
+ expect ( root ) . toMatchRenderedOutput ( < div > Preview [B]</ div > ) ;
761
+ } ) ;
762
+ // Then switch to the final state
763
+ assertLog ( [ 'B' ] ) ;
764
+ expect ( root ) . toMatchRenderedOutput ( < div > B</ div > ) ;
765
+ } ,
766
+ ) ;
767
+
768
+ // @gate enableOffscreen
769
+ it (
770
+ 'useDeferredValue does not show "previous" value when revealing a hidden ' +
771
+ 'tree (no initial value)' ,
772
+ async ( ) => {
773
+ function App ( { text} ) {
774
+ const renderedText = useDeferredValue ( text ) ;
775
+ return (
776
+ < div >
777
+ < Text text = { renderedText } />
778
+ </ div >
779
+ ) ;
780
+ }
781
+
782
+ function Container ( { text, shouldShow} ) {
783
+ return (
784
+ < Offscreen mode = { shouldShow ? 'visible' : 'hidden' } >
785
+ < App text = { text } />
786
+ </ Offscreen >
787
+ ) ;
788
+ }
789
+
790
+ const root = ReactNoop . createRoot ( ) ;
791
+
792
+ // Prerender some content
793
+ await act ( ( ) => root . render ( < Container text = "A" shouldShow = { false } /> ) ) ;
794
+ assertLog ( [ 'A' ] ) ;
795
+ expect ( root ) . toMatchRenderedOutput ( < div hidden = { true } > A</ div > ) ;
796
+
797
+ // Update the prerendered tree and reveal it at the same time. Even though
798
+ // this is a sync update, we should update B immediately rather than stay
799
+ // on the old value (A), because conceptually this is a new tree.
800
+ await act ( ( ) => root . render ( < Container text = "B" shouldShow = { true } /> ) ) ;
801
+ assertLog ( [ 'B' ] ) ;
802
+ expect ( root ) . toMatchRenderedOutput ( < div > B</ div > ) ;
803
+ } ,
804
+ ) ;
619
805
} ) ;
0 commit comments