@@ -190,4 +190,139 @@ describe('ReactDOMServerPartialHydration', () => {
190190
191191 expect ( container . firstChild . children [ 1 ] . textContent ) . toBe ( 'After' ) ;
192192 } ) ;
193+
194+ it ( 'regenerates the content if props have changed before hydration completes' , async ( ) => {
195+ let suspend = false ;
196+ let resolve ;
197+ let promise = new Promise ( resolvePromise => ( resolve = resolvePromise ) ) ;
198+ let ref = React . createRef ( ) ;
199+
200+ function Child ( { text} ) {
201+ if ( suspend ) {
202+ throw promise ;
203+ } else {
204+ return text ;
205+ }
206+ }
207+
208+ function App ( { text, className} ) {
209+ return (
210+ < div >
211+ < Suspense fallback = "Loading..." >
212+ < span ref = { ref } className = { className } >
213+ < Child text = { text } />
214+ </ span >
215+ </ Suspense >
216+ </ div >
217+ ) ;
218+ }
219+
220+ suspend = false ;
221+ let finalHTML = ReactDOMServer . renderToString (
222+ < App text = "Hello" className = "hello" /> ,
223+ ) ;
224+ let container = document . createElement ( 'div' ) ;
225+ container . innerHTML = finalHTML ;
226+
227+ let span = container . getElementsByTagName ( 'span' ) [ 0 ] ;
228+
229+ // On the client we don't have all data yet but we want to start
230+ // hydrating anyway.
231+ suspend = true ;
232+ let root = ReactDOM . unstable_createRoot ( container , { hydrate : true } ) ;
233+ root . render ( < App text = "Hello" className = "hello" /> ) ;
234+ jest . runAllTimers ( ) ;
235+
236+ expect ( ref . current ) . toBe ( null ) ;
237+ expect ( span . textContent ) . toBe ( 'Hello' ) ;
238+
239+ // Render an update, which will be higher or the same priority as pinging the hydration.
240+ root . render ( < App text = "Hi" className = "hi" /> ) ;
241+
242+ // At the same time, resolving the promise so that rendering can complete.
243+ suspend = false ;
244+ resolve ( ) ;
245+ await promise ;
246+
247+ // Flushing both of these in the same batch won't be able to hydrate so we'll
248+ // probably throw away the existing subtree.
249+ jest . runAllTimers ( ) ;
250+
251+ // Pick up the new span. In an ideal implementation this might be the same span
252+ // but patched up. At the time of writing, this will be a new span though.
253+ span = container . getElementsByTagName ( 'span' ) [ 0 ] ;
254+
255+ // We should now have fully rendered with a ref on the new span.
256+ expect ( ref . current ) . toBe ( span ) ;
257+ expect ( span . textContent ) . toBe ( 'Hi' ) ;
258+ // If we ended up hydrating the existing content, we won't have properly
259+ // patched up the tree, which might mean we haven't patched the className.
260+ expect ( span . className ) . toBe ( 'hi' ) ;
261+ } ) ;
262+
263+ it ( 'shows the fallback if props have changed before hydration completes and is still suspended' , async ( ) => {
264+ let suspend = false ;
265+ let resolve ;
266+ let promise = new Promise ( resolvePromise => ( resolve = resolvePromise ) ) ;
267+ let ref = React . createRef ( ) ;
268+
269+ function Child ( { text} ) {
270+ if ( suspend ) {
271+ throw promise ;
272+ } else {
273+ return text ;
274+ }
275+ }
276+
277+ function App ( { text, className} ) {
278+ return (
279+ < div >
280+ < Suspense fallback = "Loading..." >
281+ < span ref = { ref } className = { className } >
282+ < Child text = { text } />
283+ </ span >
284+ </ Suspense >
285+ </ div >
286+ ) ;
287+ }
288+
289+ suspend = false ;
290+ let finalHTML = ReactDOMServer . renderToString (
291+ < App text = "Hello" className = "hello" /> ,
292+ ) ;
293+ let container = document . createElement ( 'div' ) ;
294+ container . innerHTML = finalHTML ;
295+
296+ // On the client we don't have all data yet but we want to start
297+ // hydrating anyway.
298+ suspend = true ;
299+ let root = ReactDOM . unstable_createRoot ( container , { hydrate : true } ) ;
300+ root . render ( < App text = "Hello" className = "hello" /> ) ;
301+ jest . runAllTimers ( ) ;
302+
303+ expect ( ref . current ) . toBe ( null ) ;
304+
305+ // Render an update, but leave it still suspended.
306+ root . render ( < App text = "Hi" className = "hi" /> ) ;
307+
308+ // Flushing now should delete the existing content and show the fallback.
309+ jest . runAllTimers ( ) ;
310+
311+ expect ( container . getElementsByTagName ( 'span' ) . length ) . toBe ( 0 ) ;
312+ expect ( ref . current ) . toBe ( null ) ;
313+ expect ( container . textContent ) . toBe ( 'Loading...' ) ;
314+
315+ // Unsuspending shows the content.
316+ suspend = false ;
317+ resolve ( ) ;
318+ await promise ;
319+
320+ jest . runAllTimers ( ) ;
321+
322+ let span = container . getElementsByTagName ( 'span' ) [ 0 ] ;
323+ expect ( span . textContent ) . toBe ( 'Hi' ) ;
324+ expect ( span . className ) . toBe ( 'hi' ) ;
325+ expect ( ref . current ) . toBe ( span ) ;
326+ expect ( container . textContent ) . toBe ( 'Hi' ) ;
327+ } ) ;
193328} ) ;
0 commit comments