@@ -1335,6 +1335,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1335
1335
'Suspend! [Hi]' ,
1336
1336
'Loading...' ,
1337
1337
// Re-render due to lifecycle update
1338
+ 'Suspend! [Hi]' ,
1338
1339
'Loading...' ,
1339
1340
] ) ;
1340
1341
expect ( ReactNoop . getChildren ( ) ) . toEqual ( [ span ( 'Loading...' ) ] ) ;
@@ -3115,4 +3116,89 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3115
3116
expect ( root ) . toMatchRenderedOutput ( < span prop = "C" /> ) ;
3116
3117
} ,
3117
3118
) ;
3119
+
3120
+ it (
3121
+ 'regression: primary fragment fiber is not always part of setState ' +
3122
+ 'return path' ,
3123
+ async ( ) => {
3124
+ // Reproduces a bug where updates inside a suspended tree are dropped
3125
+ // because the fragment fiber we insert to wrap the hidden children is not
3126
+ // part of the return path, so it doesn't get marked during setState.
3127
+ const { useState} = React ;
3128
+ const root = ReactNoop . createRoot ( ) ;
3129
+
3130
+ function Parent ( ) {
3131
+ return (
3132
+ < >
3133
+ < Suspense fallback = { < Text text = "Loading..." /> } >
3134
+ < Child />
3135
+ </ Suspense >
3136
+ </ >
3137
+ ) ;
3138
+ }
3139
+
3140
+ let setText ;
3141
+ function Child ( ) {
3142
+ const [ text , _setText ] = useState ( 'A' ) ;
3143
+ setText = _setText ;
3144
+ return < AsyncText text = { text } /> ;
3145
+ }
3146
+
3147
+ // Mount an initial tree. Resolve A so that it doesn't suspend.
3148
+ await resolveText ( 'A' ) ;
3149
+ await ReactNoop . act ( async ( ) => {
3150
+ root . render ( < Parent /> ) ;
3151
+ } ) ;
3152
+ expect ( Scheduler ) . toHaveYielded ( [ 'A' ] ) ;
3153
+ // At this point, the setState return path follows current fiber.
3154
+ expect ( root ) . toMatchRenderedOutput ( < span prop = "A" /> ) ;
3155
+
3156
+ // Schedule another update. This will "flip" the alternate pairs.
3157
+ await resolveText ( 'B' ) ;
3158
+ await ReactNoop . act ( async ( ) => {
3159
+ setText ( 'B' ) ;
3160
+ } ) ;
3161
+ expect ( Scheduler ) . toHaveYielded ( [ 'B' ] ) ;
3162
+ // Now the setState return path follows the *alternate* fiber.
3163
+ expect ( root ) . toMatchRenderedOutput ( < span prop = "B" /> ) ;
3164
+
3165
+ // Schedule another update. This time, we'll suspend.
3166
+ await ReactNoop . act ( async ( ) => {
3167
+ setText ( 'C' ) ;
3168
+ } ) ;
3169
+ expect ( Scheduler ) . toHaveYielded ( [
3170
+ 'Suspend! [C]' ,
3171
+ 'Loading...' ,
3172
+
3173
+ 'Suspend! [C]' ,
3174
+ 'Loading...' ,
3175
+ ] ) ;
3176
+
3177
+ // Commit. This will insert a fragment fiber to wrap around the component
3178
+ // that triggered the update.
3179
+ await ReactNoop . act ( async ( ) => {
3180
+ await advanceTimers ( 250 ) ;
3181
+ } ) ;
3182
+ expect ( Scheduler ) . toHaveYielded ( [ 'Suspend! [C]' ] ) ;
3183
+ // The fragment fiber is part of the current tree, but the setState return
3184
+ // path still follows the alternate path. That means the fragment fiber is
3185
+ // not part of the return path.
3186
+ expect ( root ) . toMatchRenderedOutput (
3187
+ < >
3188
+ < span hidden = { true } prop = "B" />
3189
+ < span prop = "Loading..." />
3190
+ </ > ,
3191
+ ) ;
3192
+
3193
+ // Update again. This should unsuspend the tree.
3194
+ await resolveText ( 'D' ) ;
3195
+ await ReactNoop . act ( async ( ) => {
3196
+ setText ( 'D' ) ;
3197
+ } ) ;
3198
+ // Even though the fragment fiber is not part of the return path, we should
3199
+ // be able to finish rendering.
3200
+ expect ( Scheduler ) . toHaveYielded ( [ 'D' ] ) ;
3201
+ expect ( root ) . toMatchRenderedOutput ( < span prop = "D" /> ) ;
3202
+ } ,
3203
+ ) ;
3118
3204
} ) ;
0 commit comments