@@ -2220,6 +2220,193 @@ describe('ReactDOMFizzServer', () => {
2220
2220
} ,
2221
2221
) ;
2222
2222
2223
+ // @gate experimental
2224
+ it ( 'does not recreate the fallback if server errors and hydration suspends' , async ( ) => {
2225
+ let isClient = false ;
2226
+
2227
+ function Child ( ) {
2228
+ if ( isClient ) {
2229
+ readText ( 'Yay!' ) ;
2230
+ } else {
2231
+ throw Error ( 'Oops.' ) ;
2232
+ }
2233
+ Scheduler . unstable_yieldValue ( 'Yay!' ) ;
2234
+ return 'Yay!' ;
2235
+ }
2236
+
2237
+ const fallbackRef = React . createRef ( ) ;
2238
+ function App ( ) {
2239
+ return (
2240
+ < div >
2241
+ < Suspense fallback = { < p ref = { fallbackRef } > Loading...</ p > } >
2242
+ < span >
2243
+ < Child />
2244
+ </ span >
2245
+ </ Suspense >
2246
+ </ div >
2247
+ ) ;
2248
+ }
2249
+ await act ( async ( ) => {
2250
+ const { pipe} = ReactDOMFizzServer . renderToPipeableStream (
2251
+ < App fallbackText = "Loading..." /> ,
2252
+ {
2253
+ onError ( error ) {
2254
+ Scheduler . unstable_yieldValue ( '[s!] ' + error . message ) ;
2255
+ } ,
2256
+ } ,
2257
+ ) ;
2258
+ pipe ( writable ) ;
2259
+ } ) ;
2260
+ expect ( Scheduler ) . toHaveYielded ( [ '[s!] Oops.' ] ) ;
2261
+
2262
+ // The server could not complete this boundary, so we'll retry on the client.
2263
+ const serverFallback = container . getElementsByTagName ( 'p' ) [ 0 ] ;
2264
+ expect ( serverFallback . innerHTML ) . toBe ( 'Loading...' ) ;
2265
+
2266
+ // Hydrate the tree. This will suspend.
2267
+ isClient = true ;
2268
+ ReactDOMClient . hydrateRoot ( container , < App /> , {
2269
+ onRecoverableError ( error ) {
2270
+ Scheduler . unstable_yieldValue ( '[c!] ' + error . message ) ;
2271
+ } ,
2272
+ } ) ;
2273
+ // This should not report any errors yet.
2274
+ expect ( Scheduler ) . toFlushAndYield ( [ ] ) ;
2275
+ expect ( getVisibleChildren ( container ) ) . toEqual (
2276
+ < div >
2277
+ < p > Loading...</ p >
2278
+ </ div > ,
2279
+ ) ;
2280
+
2281
+ // Normally, hydrating after server error would force a clean client render.
2282
+ // However, it suspended so at best we'd only get the same fallback anyway.
2283
+ // We don't want to recreate the same fallback in the DOM again because
2284
+ // that's extra work and would restart animations etc. Check we don't do that.
2285
+ const clientFallback = container . getElementsByTagName ( 'p' ) [ 0 ] ;
2286
+ expect ( serverFallback ) . toBe ( clientFallback ) ;
2287
+
2288
+ // When we're able to fully hydrate, we expect a clean client render.
2289
+ await act ( async ( ) => {
2290
+ resolveText ( 'Yay!' ) ;
2291
+ } ) ;
2292
+ expect ( Scheduler ) . toFlushAndYield ( [
2293
+ 'Yay!' ,
2294
+ '[c!] The server could not finish this Suspense boundary, ' +
2295
+ 'likely due to an error during server rendering. ' +
2296
+ 'Switched to client rendering.' ,
2297
+ ] ) ;
2298
+ expect ( getVisibleChildren ( container ) ) . toEqual (
2299
+ < div >
2300
+ < span > Yay!</ span >
2301
+ </ div > ,
2302
+ ) ;
2303
+ } ) ;
2304
+
2305
+ // @gate experimental
2306
+ it (
2307
+ 'recreates the fallback if server errors and hydration suspends but ' +
2308
+ 'client receives new props' ,
2309
+ async ( ) => {
2310
+ let isClient = false ;
2311
+
2312
+ function Child ( ) {
2313
+ const value = 'Yay!' ;
2314
+ if ( isClient ) {
2315
+ readText ( value ) ;
2316
+ } else {
2317
+ throw Error ( 'Oops.' ) ;
2318
+ }
2319
+ Scheduler . unstable_yieldValue ( value ) ;
2320
+ return value ;
2321
+ }
2322
+
2323
+ const fallbackRef = React . createRef ( ) ;
2324
+ function App ( { fallbackText} ) {
2325
+ return (
2326
+ < div >
2327
+ < Suspense fallback = { < p ref = { fallbackRef } > { fallbackText } </ p > } >
2328
+ < span >
2329
+ < Child />
2330
+ </ span >
2331
+ </ Suspense >
2332
+ </ div >
2333
+ ) ;
2334
+ }
2335
+
2336
+ await act ( async ( ) => {
2337
+ const { pipe} = ReactDOMFizzServer . renderToPipeableStream (
2338
+ < App fallbackText = "Loading..." /> ,
2339
+ {
2340
+ onError ( error ) {
2341
+ Scheduler . unstable_yieldValue ( '[s!] ' + error . message ) ;
2342
+ } ,
2343
+ } ,
2344
+ ) ;
2345
+ pipe ( writable ) ;
2346
+ } ) ;
2347
+ expect ( Scheduler ) . toHaveYielded ( [ '[s!] Oops.' ] ) ;
2348
+
2349
+ const serverFallback = container . getElementsByTagName ( 'p' ) [ 0 ] ;
2350
+ expect ( serverFallback . innerHTML ) . toBe ( 'Loading...' ) ;
2351
+
2352
+ // Hydrate the tree. This will suspend.
2353
+ isClient = true ;
2354
+ const root = ReactDOMClient . hydrateRoot (
2355
+ container ,
2356
+ < App fallbackText = "Loading..." /> ,
2357
+ {
2358
+ onRecoverableError ( error ) {
2359
+ Scheduler . unstable_yieldValue ( '[c!] ' + error . message ) ;
2360
+ } ,
2361
+ } ,
2362
+ ) ;
2363
+ // This should not report any errors yet.
2364
+ expect ( Scheduler ) . toFlushAndYield ( [ ] ) ;
2365
+ expect ( getVisibleChildren ( container ) ) . toEqual (
2366
+ < div >
2367
+ < p > Loading...</ p >
2368
+ </ div > ,
2369
+ ) ;
2370
+
2371
+ // Normally, hydration after server error would force a clean client render.
2372
+ // However, that suspended so at best we'd only get a fallback anyway.
2373
+ // We don't want to replace a fallback with the same fallback because
2374
+ // that's extra work and would restart animations etc. Verify we don't do that.
2375
+ const clientFallback1 = container . getElementsByTagName ( 'p' ) [ 0 ] ;
2376
+ expect ( serverFallback ) . toBe ( clientFallback1 ) ;
2377
+
2378
+ // However, an update may have changed the fallback props. In that case we have to
2379
+ // actually force it to re-render on the client and throw away the server one.
2380
+ root . render ( < App fallbackText = "More loading..." /> ) ;
2381
+ Scheduler . unstable_flushAll ( ) ;
2382
+ jest . runAllTimers ( ) ;
2383
+ expect ( Scheduler ) . toHaveYielded ( [
2384
+ '[c!] The server could not finish this Suspense boundary, ' +
2385
+ 'likely due to an error during server rendering. ' +
2386
+ 'Switched to client rendering.' ,
2387
+ ] ) ;
2388
+ expect ( getVisibleChildren ( container ) ) . toEqual (
2389
+ < div >
2390
+ < p > More loading...</ p >
2391
+ </ div > ,
2392
+ ) ;
2393
+ // This should be a clean render without reusing DOM.
2394
+ const clientFallback2 = container . getElementsByTagName ( 'p' ) [ 0 ] ;
2395
+ expect ( clientFallback2 ) . not . toBe ( clientFallback1 ) ;
2396
+
2397
+ // Verify we can still do a clean content render after.
2398
+ await act ( async ( ) => {
2399
+ resolveText ( 'Yay!' ) ;
2400
+ } ) ;
2401
+ expect ( Scheduler ) . toFlushAndYield ( [ 'Yay!' ] ) ;
2402
+ expect ( getVisibleChildren ( container ) ) . toEqual (
2403
+ < div >
2404
+ < span > Yay!</ span >
2405
+ </ div > ,
2406
+ ) ;
2407
+ } ,
2408
+ ) ;
2409
+
2223
2410
// @gate experimental
2224
2411
it (
2225
2412
'errors during hydration force a client render at the nearest Suspense ' +
0 commit comments