@@ -17,6 +17,7 @@ let Scheduler;
1717let ReactFeatureFlags ;
1818let Suspense ;
1919let SuspenseList ;
20+ let Offscreen ;
2021let act ;
2122let IdleEventPriority ;
2223
@@ -106,6 +107,7 @@ describe('ReactDOMServerPartialHydration', () => {
106107 ReactDOMServer = require ( 'react-dom/server' ) ;
107108 Scheduler = require ( 'scheduler' ) ;
108109 Suspense = React . Suspense ;
110+ Offscreen = React . unstable_Offscreen ;
109111 if ( gate ( flags => flags . enableSuspenseList ) ) {
110112 SuspenseList = React . SuspenseList ;
111113 }
@@ -3283,6 +3285,103 @@ describe('ReactDOMServerPartialHydration', () => {
32833285 expect ( ref . current . innerHTML ) . toBe ( 'Hidden child' ) ;
32843286 } ) ;
32853287
3288+ // @gate enableOffscreen
3289+ it ( 'a visible Offscreen component acts like a fragment' , async ( ) => {
3290+ const ref = React . createRef ( ) ;
3291+
3292+ function App ( ) {
3293+ return (
3294+ < Offscreen mode = "visible" >
3295+ < span ref = { ref } > Child</ span >
3296+ </ Offscreen >
3297+ ) ;
3298+ }
3299+
3300+ const finalHTML = ReactDOMServer . renderToString ( < App /> ) ;
3301+ expect ( Scheduler ) . toHaveYielded ( [ ] ) ;
3302+
3303+ const container = document . createElement ( 'div' ) ;
3304+ container . innerHTML = finalHTML ;
3305+
3306+ // Visible Offscreen boundaries behave exactly like fragments: a
3307+ // pure indirection.
3308+ expect ( container ) . toMatchInlineSnapshot ( `
3309+ <div>
3310+ <span>
3311+ Child
3312+ </span>
3313+ </div>
3314+ ` ) ;
3315+
3316+ const span = container . getElementsByTagName ( 'span' ) [ 0 ] ;
3317+
3318+ // The tree successfully hydrates
3319+ ReactDOMClient . hydrateRoot ( container , < App /> ) ;
3320+ expect ( Scheduler ) . toFlushAndYield ( [ ] ) ;
3321+ expect ( ref . current ) . toBe ( span ) ;
3322+ } ) ;
3323+
3324+ // @gate enableOffscreen
3325+ it ( 'a hidden Offscreen component is skipped over during server rendering' , async ( ) => {
3326+ const visibleRef = React . createRef ( ) ;
3327+
3328+ function HiddenChild ( ) {
3329+ Scheduler . unstable_yieldValue ( 'HiddenChild' ) ;
3330+ return < span > Hidden</ span > ;
3331+ }
3332+
3333+ function App ( ) {
3334+ Scheduler . unstable_yieldValue ( 'App' ) ;
3335+ return (
3336+ < >
3337+ < span ref = { visibleRef } > Visible</ span >
3338+ < Offscreen mode = "hidden" >
3339+ < HiddenChild />
3340+ </ Offscreen >
3341+ </ >
3342+ ) ;
3343+ }
3344+
3345+ // During server rendering, the Child component should not be evaluated,
3346+ // because it's inside a hidden tree.
3347+ const finalHTML = ReactDOMServer . renderToString ( < App /> ) ;
3348+ expect ( Scheduler ) . toHaveYielded ( [ 'App' ] ) ;
3349+
3350+ const container = document . createElement ( 'div' ) ;
3351+ container . innerHTML = finalHTML ;
3352+
3353+ // The hidden child is not part of the server rendered HTML
3354+ expect ( container ) . toMatchInlineSnapshot ( `
3355+ <div>
3356+ <span>
3357+ Visible
3358+ </span>
3359+ </div>
3360+ ` ) ;
3361+
3362+ const visibleSpan = container . getElementsByTagName ( 'span' ) [ 0 ] ;
3363+
3364+ // The visible span successfully hydrates
3365+ ReactDOMClient . hydrateRoot ( container , < App /> ) ;
3366+ expect ( Scheduler ) . toFlushUntilNextPaint ( [ 'App' ] ) ;
3367+ expect ( visibleRef . current ) . toBe ( visibleSpan ) ;
3368+
3369+ // Subsequently, the hidden child is prerendered on the client
3370+ expect ( Scheduler ) . toFlushUntilNextPaint ( [ 'HiddenChild' ] ) ;
3371+ expect ( container ) . toMatchInlineSnapshot ( `
3372+ <div>
3373+ <span>
3374+ Visible
3375+ </span>
3376+ <span
3377+ style="display: none;"
3378+ >
3379+ Hidden
3380+ </span>
3381+ </div>
3382+ ` ) ;
3383+ } ) ;
3384+
32863385 function itHydratesWithoutMismatch ( msg , App ) {
32873386 it ( 'hydrates without mismatch ' + msg , ( ) => {
32883387 const container = document . createElement ( 'div' ) ;
0 commit comments