@@ -277,7 +277,7 @@ describe('ReactDOMServerPartialHydration', () => {
277
277
expect ( container . firstChild . children [ 1 ] . textContent ) . toBe ( 'After' ) ;
278
278
} ) ;
279
279
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 ( ) => {
281
281
let suspend = false ;
282
282
let resolve ;
283
283
let promise = new Promise ( resolvePromise => ( resolve = resolvePromise ) ) ;
@@ -331,14 +331,14 @@ describe('ReactDOMServerPartialHydration', () => {
331
331
resolve ( ) ;
332
332
await promise ;
333
333
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.
336
335
Scheduler . unstable_flushAll ( ) ;
337
336
jest . runAllTimers ( ) ;
338
337
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 ) ;
342
342
343
343
// We should now have fully rendered with a ref on the new span.
344
344
expect ( ref . current ) . toBe ( span ) ;
@@ -562,7 +562,87 @@ describe('ReactDOMServerPartialHydration', () => {
562
562
expect ( container . textContent ) . toBe ( 'Hi Hi' ) ;
563
563
} ) ;
564
564
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 ( ) => {
566
646
let suspend = false ;
567
647
let resolve ;
568
648
let promise = new Promise ( resolvePromise => ( resolve = resolvePromise ) ) ;
@@ -630,14 +710,13 @@ describe('ReactDOMServerPartialHydration', () => {
630
710
resolve ( ) ;
631
711
await promise ;
632
712
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.
635
714
Scheduler . unstable_flushAll ( ) ;
636
715
jest . runAllTimers ( ) ;
637
716
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 ) ;
641
720
642
721
// We should now have fully rendered with a ref on the new span.
643
722
expect ( ref . current ) . toBe ( span ) ;
@@ -1421,4 +1500,85 @@ describe('ReactDOMServerPartialHydration', () => {
1421
1500
expect ( ref1 . current ) . toBe ( span1 ) ;
1422
1501
expect ( ref2 . current ) . toBe ( span2 ) ;
1423
1502
} ) ;
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
+ } ) ;
1424
1584
} ) ;
0 commit comments