@@ -616,4 +616,190 @@ describe('ReactDeferredValue', () => {
616616 assertLog ( [ ] ) ;
617617 expect ( root ) . toMatchRenderedOutput ( < div > Final</ div > ) ;
618618 } ) ;
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+ ) ;
619805} ) ;
0 commit comments