@@ -277,7 +277,7 @@ describe('ReactDOMServerPartialHydration', () => {
277277 expect ( container . firstChild . children [ 1 ] . textContent ) . toBe ( 'After' ) ;
278278 } ) ;
279279
280- it ( 'regenerates the content if props have changed before hydration completes ' , async ( ) => {
280+ it ( 'blocks updates to hydrate the content first if props have changed' , async ( ) => {
281281 let suspend = false ;
282282 let resolve ;
283283 let promise = new Promise ( resolvePromise => ( resolve = resolvePromise ) ) ;
@@ -331,14 +331,14 @@ describe('ReactDOMServerPartialHydration', () => {
331331 resolve ( ) ;
332332 await promise ;
333333
334- // Flushing both of these in the same batch won't be able to hydrate so we'll
335- // probably throw away the existing subtree.
334+ // This should first complete the hydration and then flush the update onto the hydrated state.
336335 Scheduler . unstable_flushAll ( ) ;
337336 jest . runAllTimers ( ) ;
338337
339- // Pick up the new span. In an ideal implementation this might be the same span
340- // but patched up. At the time of writing, this will be a new span though.
341- span = container . getElementsByTagName ( 'span' ) [ 0 ] ;
338+ // The new span should be the same since we should have successfully hydrated
339+ // before changing it.
340+ let newSpan = container . getElementsByTagName ( 'span' ) [ 0 ] ;
341+ expect ( span ) . toBe ( newSpan ) ;
342342
343343 // We should now have fully rendered with a ref on the new span.
344344 expect ( ref . current ) . toBe ( span ) ;
@@ -562,7 +562,87 @@ describe('ReactDOMServerPartialHydration', () => {
562562 expect ( container . textContent ) . toBe ( 'Hi Hi' ) ;
563563 } ) ;
564564
565- it ( 'regenerates the content if context has changed before hydration completes' , async ( ) => {
565+ it ( 'hydrates first if props changed but we are able to resolve within a timeout' , async ( ) => {
566+ let suspend = false ;
567+ let resolve ;
568+ let promise = new Promise ( resolvePromise => ( resolve = resolvePromise ) ) ;
569+ let ref = React . createRef ( ) ;
570+
571+ function Child ( { text} ) {
572+ if ( suspend ) {
573+ throw promise ;
574+ } else {
575+ return text ;
576+ }
577+ }
578+
579+ function App ( { text, className} ) {
580+ return (
581+ < div >
582+ < Suspense fallback = "Loading..." >
583+ < span ref = { ref } className = { className } >
584+ < Child text = { text } />
585+ </ span >
586+ </ Suspense >
587+ </ div >
588+ ) ;
589+ }
590+
591+ suspend = false ;
592+ let finalHTML = ReactDOMServer . renderToString (
593+ < App text = "Hello" className = "hello" /> ,
594+ ) ;
595+ let container = document . createElement ( 'div' ) ;
596+ container . innerHTML = finalHTML ;
597+
598+ let span = container . getElementsByTagName ( 'span' ) [ 0 ] ;
599+
600+ // On the client we don't have all data yet but we want to start
601+ // hydrating anyway.
602+ suspend = true ;
603+ let root = ReactDOM . unstable_createRoot ( container , { hydrate : true } ) ;
604+ root . render ( < App text = "Hello" className = "hello" /> ) ;
605+ Scheduler . unstable_flushAll ( ) ;
606+ jest . runAllTimers ( ) ;
607+
608+ expect ( ref . current ) . toBe ( null ) ;
609+ expect ( container . textContent ) . toBe ( 'Hello' ) ;
610+
611+ // Render an update with a long timeout.
612+ React . unstable_withSuspenseConfig (
613+ ( ) => root . render ( < App text = "Hi" className = "hi" /> ) ,
614+ { timeoutMs : 5000 } ,
615+ ) ;
616+
617+ // This shouldn't force the fallback yet.
618+ Scheduler . unstable_flushAll ( ) ;
619+
620+ expect ( ref . current ) . toBe ( null ) ;
621+ expect ( container . textContent ) . toBe ( 'Hello' ) ;
622+
623+ // Resolving the promise so that rendering can complete.
624+ suspend = false ;
625+ resolve ( ) ;
626+ await promise ;
627+
628+ // This should first complete the hydration and then flush the update onto the hydrated state.
629+ Scheduler . unstable_flushAll ( ) ;
630+ jest . runAllTimers ( ) ;
631+
632+ // The new span should be the same since we should have successfully hydrated
633+ // before changing it.
634+ let newSpan = container . getElementsByTagName ( 'span' ) [ 0 ] ;
635+ expect ( span ) . toBe ( newSpan ) ;
636+
637+ // We should now have fully rendered with a ref on the new span.
638+ expect ( ref . current ) . toBe ( span ) ;
639+ expect ( container . textContent ) . toBe ( 'Hi' ) ;
640+ // If we ended up hydrating the existing content, we won't have properly
641+ // patched up the tree, which might mean we haven't patched the className.
642+ expect ( span . className ) . toBe ( 'hi' ) ;
643+ } ) ;
644+
645+ it ( 'blocks the update to hydrate first if context has changed' , async ( ) => {
566646 let suspend = false ;
567647 let resolve ;
568648 let promise = new Promise ( resolvePromise => ( resolve = resolvePromise ) ) ;
@@ -630,14 +710,13 @@ describe('ReactDOMServerPartialHydration', () => {
630710 resolve ( ) ;
631711 await promise ;
632712
633- // Flushing both of these in the same batch won't be able to hydrate so we'll
634- // probably throw away the existing subtree.
713+ // This should first complete the hydration and then flush the update onto the hydrated state.
635714 Scheduler . unstable_flushAll ( ) ;
636715 jest . runAllTimers ( ) ;
637716
638- // Pick up the new span. In an ideal implementation this might be the same span
639- // but patched up. At the time of writing, this will be a new span though.
640- span = container . getElementsByTagName ( ' span' ) [ 0 ] ;
717+ // Since this should have been hydrated, this should still be the same span.
718+ let newSpan = container . getElementsByTagName ( ' span' ) [ 0 ] ;
719+ expect ( newSpan ) . toBe ( span ) ;
641720
642721 // We should now have fully rendered with a ref on the new span.
643722 expect ( ref . current ) . toBe ( span ) ;
@@ -1421,4 +1500,85 @@ describe('ReactDOMServerPartialHydration', () => {
14211500 expect ( ref1 . current ) . toBe ( span1 ) ;
14221501 expect ( ref2 . current ) . toBe ( span2 ) ;
14231502 } ) ;
1503+
1504+ it ( 'regenerates if it cannot hydrate before changes to props/context expire' , async ( ) => {
1505+ let suspend = false ;
1506+ let promise = new Promise ( resolvePromise => { } ) ;
1507+ let ref = React . createRef ( ) ;
1508+ let ClassName = React . createContext ( null ) ;
1509+
1510+ function Child ( { text} ) {
1511+ let className = React . useContext ( ClassName ) ;
1512+ if ( suspend && className !== 'hi' && text !== 'Hi' ) {
1513+ // Never suspends on the newer data.
1514+ throw promise ;
1515+ } else {
1516+ return (
1517+ < span ref = { ref } className = { className } >
1518+ { text }
1519+ </ span >
1520+ ) ;
1521+ }
1522+ }
1523+
1524+ function App ( { text, className} ) {
1525+ return (
1526+ < div >
1527+ < Suspense fallback = "Loading..." >
1528+ < Child text = { text } />
1529+ </ Suspense >
1530+ </ div >
1531+ ) ;
1532+ }
1533+
1534+ suspend = false ;
1535+ let finalHTML = ReactDOMServer . renderToString (
1536+ < ClassName . Provider value = { 'hello' } >
1537+ < App text = "Hello" />
1538+ </ ClassName . Provider > ,
1539+ ) ;
1540+ let container = document . createElement ( 'div' ) ;
1541+ container . innerHTML = finalHTML ;
1542+
1543+ let span = container . getElementsByTagName ( 'span' ) [ 0 ] ;
1544+
1545+ // On the client we don't have all data yet but we want to start
1546+ // hydrating anyway.
1547+ suspend = true ;
1548+ let root = ReactDOM . unstable_createRoot ( container , { hydrate : true } ) ;
1549+ root . render (
1550+ < ClassName . Provider value = { 'hello' } >
1551+ < App text = "Hello" />
1552+ </ ClassName . Provider > ,
1553+ ) ;
1554+ Scheduler . unstable_flushAll ( ) ;
1555+ jest . runAllTimers ( ) ;
1556+
1557+ expect ( ref . current ) . toBe ( null ) ;
1558+ expect ( span . textContent ) . toBe ( 'Hello' ) ;
1559+
1560+ // Render an update, which will be higher or the same priority as pinging the hydration.
1561+ // The new update doesn't suspend.
1562+ root . render (
1563+ < ClassName . Provider value = { 'hi' } >
1564+ < App text = "Hi" />
1565+ </ ClassName . Provider > ,
1566+ ) ;
1567+
1568+ // Since we're still suspended on the original data, we can't hydrate.
1569+ // This will force all expiration times to flush.
1570+ Scheduler . unstable_flushAll ( ) ;
1571+ jest . runAllTimers ( ) ;
1572+
1573+ // This will now be a new span because we weren't able to hydrate before
1574+ let newSpan = container . getElementsByTagName ( 'span' ) [ 0 ] ;
1575+ expect ( newSpan ) . not . toBe ( span ) ;
1576+
1577+ // We should now have fully rendered with a ref on the new span.
1578+ expect ( ref . current ) . toBe ( newSpan ) ;
1579+ expect ( newSpan . textContent ) . toBe ( 'Hi' ) ;
1580+ // If we ended up hydrating the existing content, we won't have properly
1581+ // patched up the tree, which might mean we haven't patched the className.
1582+ expect ( newSpan . className ) . toBe ( 'hi' ) ;
1583+ } ) ;
14241584} ) ;
0 commit comments