@@ -2220,6 +2220,193 @@ 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 (
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+
22232410 // @gate experimental
22242411 it (
22252412 'errors during hydration force a client render at the nearest Suspense ' +
0 commit comments