@@ -869,16 +869,16 @@ describe('ReactDOMFizzServer', () => {
869869 } ) ;
870870
871871 // 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 ( [ ] ) ;
876873 expect ( getVisibleChildren ( container ) ) . toEqual ( < div > Loading...</ div > ) ;
877874
878875 // We now resolve it on the client.
879876 resolveText ( 'Hello' ) ;
880877
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+ ] ) ;
882882
883883 // The client rendered HTML is now in place.
884884 expect ( getVisibleChildren ( container ) ) . toEqual (
@@ -2220,6 +2220,286 @@ describe('ReactDOMFizzServer', () => {
22202220 } ,
22212221 ) ;
22222222
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+
22232503 // @gate experimental
22242504 it (
22252505 'errors during hydration force a client render at the nearest Suspense ' +
@@ -2293,25 +2573,25 @@ describe('ReactDOMFizzServer', () => {
22932573 } ,
22942574 } ) ;
22952575
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 ( [ ] ) ;
23032578 expect ( getVisibleChildren ( container ) ) . toEqual (
23042579 < div >
23052580 < span />
2306- Loading...
2581+ < span > Yay! </ span >
23072582 < span />
23082583 </ div > ,
23092584 ) ;
23102585
23112586 await act ( async ( ) => {
23122587 resolveText ( 'Yay!' ) ;
23132588 } ) ;
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+ ] ) ;
23152595 expect ( getVisibleChildren ( container ) ) . toEqual (
23162596 < div >
23172597 < span />
0 commit comments