@@ -5,6 +5,7 @@ let ReactNoop;
5
5
let Scheduler ;
6
6
let act ;
7
7
let use ;
8
+ let useState ;
8
9
let Suspense ;
9
10
let startTransition ;
10
11
let pendingTextRequests ;
@@ -18,6 +19,7 @@ describe('ReactThenable', () => {
18
19
Scheduler = require ( 'scheduler' ) ;
19
20
act = require ( 'jest-react' ) . act ;
20
21
use = React . use ;
22
+ useState = React . useState ;
21
23
Suspense = React . Suspense ;
22
24
startTransition = React . startTransition ;
23
25
@@ -668,4 +670,92 @@ describe('ReactThenable', () => {
668
670
expect ( Scheduler ) . toHaveYielded ( [ 'Hi' ] ) ;
669
671
expect ( root ) . toMatchRenderedOutput ( 'Hi' ) ;
670
672
} ) ;
673
+
674
+ // @gate enableUseHook
675
+ test ( 'does not suspend indefinitely if an interleaved update was skipped' , async ( ) => {
676
+ function Child ( { childShouldSuspend} ) {
677
+ return (
678
+ < Text
679
+ text = {
680
+ childShouldSuspend
681
+ ? use ( getAsyncText ( 'Will never resolve' ) )
682
+ : 'Child'
683
+ }
684
+ />
685
+ ) ;
686
+ }
687
+
688
+ let setChildShouldSuspend ;
689
+ let setShowChild ;
690
+ function Parent ( ) {
691
+ const [ showChild , _setShowChild ] = useState ( true ) ;
692
+ setShowChild = _setShowChild ;
693
+
694
+ const [ childShouldSuspend , _setChildShouldSuspend ] = useState ( false ) ;
695
+ setChildShouldSuspend = _setChildShouldSuspend ;
696
+
697
+ Scheduler . unstable_yieldValue (
698
+ `childShouldSuspend: ${ childShouldSuspend } , showChild: ${ showChild } ` ,
699
+ ) ;
700
+ return showChild ? (
701
+ < Child childShouldSuspend = { childShouldSuspend } />
702
+ ) : (
703
+ < Text text = "(empty)" />
704
+ ) ;
705
+ }
706
+
707
+ const root = ReactNoop . createRoot ( ) ;
708
+ await act ( ( ) => {
709
+ root . render ( < Parent /> ) ;
710
+ } ) ;
711
+ expect ( Scheduler ) . toHaveYielded ( [
712
+ 'childShouldSuspend: false, showChild: true' ,
713
+ 'Child' ,
714
+ ] ) ;
715
+ expect ( root ) . toMatchRenderedOutput ( 'Child' ) ;
716
+
717
+ await act ( ( ) => {
718
+ // Perform an update that causes the app to suspend
719
+ startTransition ( ( ) => {
720
+ setChildShouldSuspend ( true ) ;
721
+ } ) ;
722
+ expect ( Scheduler ) . toFlushAndYieldThrough ( [
723
+ 'childShouldSuspend: true, showChild: true' ,
724
+ ] ) ;
725
+ // While the update is in progress, schedule another update.
726
+ startTransition ( ( ) => {
727
+ setShowChild ( false ) ;
728
+ } ) ;
729
+ } ) ;
730
+ expect ( Scheduler ) . toHaveYielded ( [
731
+ // Because the interleaved update is not higher priority than what we were
732
+ // already working on, it won't interrupt. The first update will continue,
733
+ // and will suspend.
734
+ 'Async text requested [Will never resolve]' ,
735
+
736
+ // Instead of waiting for the promise to resolve, React notices there's
737
+ // another pending update that it hasn't tried yet. It will switch to
738
+ // rendering that instead.
739
+ //
740
+ // This time, the update hides the component that previous was suspending,
741
+ // so it finishes successfully.
742
+ 'childShouldSuspend: false, showChild: false' ,
743
+ '(empty)' ,
744
+
745
+ // Finally, React attempts to render the first update again. It also
746
+ // finishes successfully, because it was rebased on top of the update that
747
+ // hid the suspended component.
748
+ // NOTE: These this render happened to not be entangled with the previous
749
+ // one. If they had been, this update would have been included in the
750
+ // previous render, and there wouldn't be an extra one here. This could
751
+ // change if we change our entanglement heurstics. Semantically, it
752
+ // shouldn't matter, though in general we try to work on transitions in
753
+ // parallel whenever possible. So even though in this particular case, the
754
+ // extra render is unnecessary, it's a nice property that it wasn't
755
+ // entangled with the other transition.
756
+ 'childShouldSuspend: true, showChild: false' ,
757
+ '(empty)' ,
758
+ ] ) ;
759
+ expect ( root ) . toMatchRenderedOutput ( '(empty)' ) ;
760
+ } ) ;
671
761
} ) ;
0 commit comments