@@ -869,16 +869,16 @@ describe('ReactDOMFizzServer', () => {
869
869
} ) ;
870
870
871
871
// We still can't render it on the client.
872
- expect ( Scheduler ) . toFlushAndYield ( [
873
- 'The server could not finish this Suspense boundary, likely due to an ' +
874
- 'error during server rendering. Switched to client rendering.' ,
875
- ] ) ;
872
+ expect ( Scheduler ) . toFlushAndYield ( [ ] ) ;
876
873
expect ( getVisibleChildren ( container ) ) . toEqual ( < div > Loading...</ div > ) ;
877
874
878
875
// We now resolve it on the client.
879
876
resolveText ( 'Hello' ) ;
880
877
881
- Scheduler . unstable_flushAll ( ) ;
878
+ expect ( Scheduler ) . toFlushAndYield ( [
879
+ 'The server could not finish this Suspense boundary, likely due to an ' +
880
+ 'error during server rendering. Switched to client rendering.' ,
881
+ ] ) ;
882
882
883
883
// The client rendered HTML is now in place.
884
884
expect ( getVisibleChildren ( container ) ) . toEqual (
@@ -2220,6 +2220,286 @@ 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 ( < App /> , {
2251
+ onError ( error ) {
2252
+ Scheduler . unstable_yieldValue ( '[s!] ' + error . message ) ;
2253
+ } ,
2254
+ } ) ;
2255
+ pipe ( writable ) ;
2256
+ } ) ;
2257
+ expect ( Scheduler ) . toHaveYielded ( [ '[s!] Oops.' ] ) ;
2258
+
2259
+ // The server could not complete this boundary, so we'll retry on the client.
2260
+ const serverFallback = container . getElementsByTagName ( 'p' ) [ 0 ] ;
2261
+ expect ( serverFallback . innerHTML ) . toBe ( 'Loading...' ) ;
2262
+
2263
+ // Hydrate the tree. This will suspend.
2264
+ isClient = true ;
2265
+ ReactDOMClient . hydrateRoot ( container , < App /> , {
2266
+ onRecoverableError ( error ) {
2267
+ Scheduler . unstable_yieldValue ( '[c!] ' + error . message ) ;
2268
+ } ,
2269
+ } ) ;
2270
+ // This should not report any errors yet.
2271
+ expect ( Scheduler ) . toFlushAndYield ( [ ] ) ;
2272
+ expect ( getVisibleChildren ( container ) ) . toEqual (
2273
+ < div >
2274
+ < p > Loading...</ p >
2275
+ </ div > ,
2276
+ ) ;
2277
+
2278
+ // Normally, hydrating after server error would force a clean client render.
2279
+ // However, it suspended so at best we'd only get the same fallback anyway.
2280
+ // We don't want to recreate the same fallback in the DOM again because
2281
+ // that's extra work and would restart animations etc. Check we don't do that.
2282
+ const clientFallback = container . getElementsByTagName ( 'p' ) [ 0 ] ;
2283
+ expect ( serverFallback ) . toBe ( clientFallback ) ;
2284
+
2285
+ // When we're able to fully hydrate, we expect a clean client render.
2286
+ await act ( async ( ) => {
2287
+ resolveText ( 'Yay!' ) ;
2288
+ } ) ;
2289
+ expect ( Scheduler ) . toFlushAndYield ( [
2290
+ 'Yay!' ,
2291
+ '[c!] The server could not finish this Suspense boundary, ' +
2292
+ 'likely due to an error during server rendering. ' +
2293
+ 'Switched to client rendering.' ,
2294
+ ] ) ;
2295
+ expect ( getVisibleChildren ( container ) ) . toEqual (
2296
+ < div >
2297
+ < span > Yay!</ span >
2298
+ </ div > ,
2299
+ ) ;
2300
+ } ) ;
2301
+
2302
+ // @gate experimental
2303
+ it (
2304
+ 'does not recreate the fallback if server errors and hydration suspends ' +
2305
+ 'and root receives a transition' ,
2306
+ async ( ) => {
2307
+ let isClient = false ;
2308
+
2309
+ function Child ( { color} ) {
2310
+ if ( isClient ) {
2311
+ readText ( 'Yay!' ) ;
2312
+ } else {
2313
+ throw Error ( 'Oops.' ) ;
2314
+ }
2315
+ Scheduler . unstable_yieldValue ( 'Yay! (' + color + ')' ) ;
2316
+ return 'Yay! (' + color + ')' ;
2317
+ }
2318
+
2319
+ const fallbackRef = React . createRef ( ) ;
2320
+ function App ( { color} ) {
2321
+ return (
2322
+ < div >
2323
+ < Suspense fallback = { < p ref = { fallbackRef } > Loading...</ p > } >
2324
+ < span >
2325
+ < Child color = { color } />
2326
+ </ span >
2327
+ </ Suspense >
2328
+ </ div >
2329
+ ) ;
2330
+ }
2331
+ await act ( async ( ) => {
2332
+ const { pipe} = ReactDOMFizzServer . renderToPipeableStream (
2333
+ < App color = "red" /> ,
2334
+ {
2335
+ onError ( error ) {
2336
+ Scheduler . unstable_yieldValue ( '[s!] ' + error . message ) ;
2337
+ } ,
2338
+ } ,
2339
+ ) ;
2340
+ pipe ( writable ) ;
2341
+ } ) ;
2342
+ expect ( Scheduler ) . toHaveYielded ( [ '[s!] Oops.' ] ) ;
2343
+
2344
+ // The server could not complete this boundary, so we'll retry on the client.
2345
+ const serverFallback = container . getElementsByTagName ( 'p' ) [ 0 ] ;
2346
+ expect ( serverFallback . innerHTML ) . toBe ( 'Loading...' ) ;
2347
+
2348
+ // Hydrate the tree. This will suspend.
2349
+ isClient = true ;
2350
+ const root = ReactDOMClient . hydrateRoot ( container , < App color = "red" /> , {
2351
+ onRecoverableError ( error ) {
2352
+ Scheduler . unstable_yieldValue ( '[c!] ' + error . message ) ;
2353
+ } ,
2354
+ } ) ;
2355
+ // This should not report any errors yet.
2356
+ expect ( Scheduler ) . toFlushAndYield ( [ ] ) ;
2357
+ expect ( getVisibleChildren ( container ) ) . toEqual (
2358
+ < div >
2359
+ < p > Loading...</ p >
2360
+ </ div > ,
2361
+ ) ;
2362
+
2363
+ // Normally, hydrating after server error would force a clean client render.
2364
+ // However, it suspended so at best we'd only get the same fallback anyway.
2365
+ // We don't want to recreate the same fallback in the DOM again because
2366
+ // that's extra work and would restart animations etc. Check we don't do that.
2367
+ const clientFallback = container . getElementsByTagName ( 'p' ) [ 0 ] ;
2368
+ expect ( serverFallback ) . toBe ( clientFallback ) ;
2369
+
2370
+ // Transition updates shouldn't recreate the fallback either.
2371
+ React . startTransition ( ( ) => {
2372
+ root . render ( < App color = "blue" /> ) ;
2373
+ } ) ;
2374
+ Scheduler . unstable_flushAll ( ) ;
2375
+ jest . runAllTimers ( ) ;
2376
+ const clientFallback2 = container . getElementsByTagName ( 'p' ) [ 0 ] ;
2377
+ expect ( clientFallback2 ) . toBe ( serverFallback ) ;
2378
+
2379
+ // When we're able to fully hydrate, we expect a clean client render.
2380
+ await act ( async ( ) => {
2381
+ resolveText ( 'Yay!' ) ;
2382
+ } ) ;
2383
+ expect ( Scheduler ) . toFlushAndYield ( [
2384
+ 'Yay! (red)' ,
2385
+ '[c!] The server could not finish this Suspense boundary, ' +
2386
+ 'likely due to an error during server rendering. ' +
2387
+ 'Switched to client rendering.' ,
2388
+ 'Yay! (blue)' ,
2389
+ ] ) ;
2390
+ expect ( getVisibleChildren ( container ) ) . toEqual (
2391
+ < div >
2392
+ < span > Yay! (blue)</ span >
2393
+ </ div > ,
2394
+ ) ;
2395
+ } ,
2396
+ ) ;
2397
+
2398
+ // @gate experimental
2399
+ it (
2400
+ 'recreates the fallback if server errors and hydration suspends but ' +
2401
+ 'client receives new props' ,
2402
+ async ( ) => {
2403
+ let isClient = false ;
2404
+
2405
+ function Child ( ) {
2406
+ const value = 'Yay!' ;
2407
+ if ( isClient ) {
2408
+ readText ( value ) ;
2409
+ } else {
2410
+ throw Error ( 'Oops.' ) ;
2411
+ }
2412
+ Scheduler . unstable_yieldValue ( value ) ;
2413
+ return value ;
2414
+ }
2415
+
2416
+ const fallbackRef = React . createRef ( ) ;
2417
+ function App ( { fallbackText} ) {
2418
+ return (
2419
+ < div >
2420
+ < Suspense fallback = { < p ref = { fallbackRef } > { fallbackText } </ p > } >
2421
+ < span >
2422
+ < Child />
2423
+ </ span >
2424
+ </ Suspense >
2425
+ </ div >
2426
+ ) ;
2427
+ }
2428
+
2429
+ await act ( async ( ) => {
2430
+ const { pipe} = ReactDOMFizzServer . renderToPipeableStream (
2431
+ < App fallbackText = "Loading..." /> ,
2432
+ {
2433
+ onError ( error ) {
2434
+ Scheduler . unstable_yieldValue ( '[s!] ' + error . message ) ;
2435
+ } ,
2436
+ } ,
2437
+ ) ;
2438
+ pipe ( writable ) ;
2439
+ } ) ;
2440
+ expect ( Scheduler ) . toHaveYielded ( [ '[s!] Oops.' ] ) ;
2441
+
2442
+ const serverFallback = container . getElementsByTagName ( 'p' ) [ 0 ] ;
2443
+ expect ( serverFallback . innerHTML ) . toBe ( 'Loading...' ) ;
2444
+
2445
+ // Hydrate the tree. This will suspend.
2446
+ isClient = true ;
2447
+ const root = ReactDOMClient . hydrateRoot (
2448
+ container ,
2449
+ < App fallbackText = "Loading..." /> ,
2450
+ {
2451
+ onRecoverableError ( error ) {
2452
+ Scheduler . unstable_yieldValue ( '[c!] ' + error . message ) ;
2453
+ } ,
2454
+ } ,
2455
+ ) ;
2456
+ // This should not report any errors yet.
2457
+ expect ( Scheduler ) . toFlushAndYield ( [ ] ) ;
2458
+ expect ( getVisibleChildren ( container ) ) . toEqual (
2459
+ < div >
2460
+ < p > Loading...</ p >
2461
+ </ div > ,
2462
+ ) ;
2463
+
2464
+ // Normally, hydration after server error would force a clean client render.
2465
+ // However, that suspended so at best we'd only get a fallback anyway.
2466
+ // We don't want to replace a fallback with the same fallback because
2467
+ // that's extra work and would restart animations etc. Verify we don't do that.
2468
+ const clientFallback1 = container . getElementsByTagName ( 'p' ) [ 0 ] ;
2469
+ expect ( serverFallback ) . toBe ( clientFallback1 ) ;
2470
+
2471
+ // However, an update may have changed the fallback props. In that case we have to
2472
+ // actually force it to re-render on the client and throw away the server one.
2473
+ root . render ( < App fallbackText = "More loading..." /> ) ;
2474
+ Scheduler . unstable_flushAll ( ) ;
2475
+ jest . runAllTimers ( ) ;
2476
+ expect ( Scheduler ) . toHaveYielded ( [
2477
+ '[c!] The server could not finish this Suspense boundary, ' +
2478
+ 'likely due to an error during server rendering. ' +
2479
+ 'Switched to client rendering.' ,
2480
+ ] ) ;
2481
+ expect ( getVisibleChildren ( container ) ) . toEqual (
2482
+ < div >
2483
+ < p > More loading...</ p >
2484
+ </ div > ,
2485
+ ) ;
2486
+ // This should be a clean render without reusing DOM.
2487
+ const clientFallback2 = container . getElementsByTagName ( 'p' ) [ 0 ] ;
2488
+ expect ( clientFallback2 ) . not . toBe ( clientFallback1 ) ;
2489
+
2490
+ // Verify we can still do a clean content render after.
2491
+ await act ( async ( ) => {
2492
+ resolveText ( 'Yay!' ) ;
2493
+ } ) ;
2494
+ expect ( Scheduler ) . toFlushAndYield ( [ 'Yay!' ] ) ;
2495
+ expect ( getVisibleChildren ( container ) ) . toEqual (
2496
+ < div >
2497
+ < span > Yay!</ span >
2498
+ </ div > ,
2499
+ ) ;
2500
+ } ,
2501
+ ) ;
2502
+
2223
2503
// @gate experimental
2224
2504
it (
2225
2505
'errors during hydration force a client render at the nearest Suspense ' +
@@ -2293,25 +2573,25 @@ describe('ReactDOMFizzServer', () => {
2293
2573
} ,
2294
2574
} ) ;
2295
2575
2296
- // An error logged but instead of surfacing it to the UI, we switched
2297
- // to client rendering.
2298
- expect ( Scheduler ) . toFlushAndYield ( [
2299
- 'Hydration error' ,
2300
- 'There was an error while hydrating this Suspense boundary. Switched ' +
2301
- 'to client rendering.' ,
2302
- ] ) ;
2576
+ // An error happened but instead of surfacing it to the UI, we suspended.
2577
+ expect ( Scheduler ) . toFlushAndYield ( [ ] ) ;
2303
2578
expect ( getVisibleChildren ( container ) ) . toEqual (
2304
2579
< div >
2305
2580
< span />
2306
- Loading...
2581
+ < span > Yay! </ span >
2307
2582
< span />
2308
2583
</ div > ,
2309
2584
) ;
2310
2585
2311
2586
await act ( async ( ) => {
2312
2587
resolveText ( 'Yay!' ) ;
2313
2588
} ) ;
2314
- expect ( Scheduler ) . toFlushAndYield ( [ 'Yay!' ] ) ;
2589
+ expect ( Scheduler ) . toFlushAndYield ( [
2590
+ 'Yay!' ,
2591
+ 'Hydration error' ,
2592
+ 'There was an error while hydrating this Suspense boundary. Switched ' +
2593
+ 'to client rendering.' ,
2594
+ ] ) ;
2315
2595
expect ( getVisibleChildren ( container ) ) . toEqual (
2316
2596
< div >
2317
2597
< span />
0 commit comments