@@ -5,6 +5,7 @@ let ReactNoop;
55let Scheduler ;
66let act ;
77let use ;
8+ let useState ;
89let Suspense ;
910let startTransition ;
1011let pendingTextRequests ;
@@ -18,6 +19,7 @@ describe('ReactThenable', () => {
1819 Scheduler = require ( 'scheduler' ) ;
1920 act = require ( 'jest-react' ) . act ;
2021 use = React . use ;
22+ useState = React . useState ;
2123 Suspense = React . Suspense ;
2224 startTransition = React . startTransition ;
2325
@@ -668,4 +670,92 @@ describe('ReactThenable', () => {
668670 expect ( Scheduler ) . toHaveYielded ( [ 'Hi' ] ) ;
669671 expect ( root ) . toMatchRenderedOutput ( 'Hi' ) ;
670672 } ) ;
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+ } ) ;
671761} ) ;
0 commit comments