@@ -8,6 +8,7 @@ import React, {
88 Fragment ,
99 createContext ,
1010 useState ,
11+ useRef ,
1112 useEffect ,
1213 useLayoutEffect ,
1314 memo
@@ -202,9 +203,43 @@ describe('suspense', () => {
202203 resolve ( ) . then ( assert ) . catch ( assert ) ;
203204 } ) ;
204205
205- it ( 'should reset hooks of components' , ( ) => {
206+ it ( 'should reset hooks when a component subtree suspends during mount' , async ( ) => {
207+ let resolve ;
208+ let resolved = false ;
209+ let initializations = 0 ;
210+ const promise = new Promise ( r => {
211+ resolve = ( ) => {
212+ resolved = true ;
213+ r ( ) ;
214+ return promise ;
215+ } ;
216+ } ) ;
217+
218+ function App ( ) {
219+ const [ value ] = useState ( ( ) => ++ initializations ) ;
220+ if ( ! resolved ) throw promise ;
221+ return < p > { value } </ p > ;
222+ }
223+
224+ render (
225+ < Suspense fallback = "loading" >
226+ < App />
227+ </ Suspense > ,
228+ scratch
229+ ) ;
230+ rerender ( ) ;
231+ expect ( scratch . textContent ) . to . equal ( 'loading' ) ;
232+
233+ await resolve ( ) ;
234+ rerender ( ) ;
235+ expect ( scratch . innerHTML ) . to . equal ( '<p>2</p>' ) ;
236+ expect ( initializations ) . to . equal ( 2 ) ;
237+ } ) ;
238+
239+ it ( 'should preserve hooks of mounted components' , ( ) => {
206240 /** @type {(v) => void } */
207241 let set ;
242+ let initialRef ;
208243 const LazyComp = ( { name } ) => < div > Hello from { name } </ div > ;
209244
210245 /** @type {() => Promise<void> } */
@@ -222,7 +257,10 @@ describe('suspense', () => {
222257
223258 const Parent = ( { children } ) => {
224259 const [ state , setState ] = useState ( false ) ;
260+ const ref = useRef ( { } ) ;
225261 set = setState ;
262+ if ( ! initialRef ) initialRef = ref ;
263+ else expect ( ref ) . to . equal ( initialRef ) ;
226264
227265 return (
228266 < div >
@@ -249,15 +287,21 @@ describe('suspense', () => {
249287
250288 return resolve ( ) . then ( ( ) => {
251289 rerender ( ) ;
252- expect ( scratch . innerHTML ) . to . eql ( `<div><p>hi</p></div>` ) ;
290+ expect ( scratch . innerHTML ) . to . eql (
291+ `<div><p>hi</p><div>Hello from LazyComp</div></div>`
292+ ) ;
253293 } ) ;
254294 } ) ;
255295
256- it ( 'should call effect cleanups' , ( ) => {
296+ it ( 'should call effect cleanups and setups when hiding and revealing' , async ( ) => {
257297 /** @type {(v) => void } */
258298 let set ;
299+ const effectSetupSpy = vi . fn ( ) ;
259300 const effectSpy = vi . fn ( ) ;
301+ const effectWithoutCleanupSpy = vi . fn ( ) ;
302+ const layoutEffectSetupSpy = vi . fn ( ) ;
260303 const layoutEffectSpy = vi . fn ( ) ;
304+ const layoutEffectWithoutCleanupSpy = vi . fn ( ) ;
261305 const LazyComp = ( { name } ) => < div > Hello from { name } </ div > ;
262306
263307 /** @type {() => Promise<void> } */
@@ -277,16 +321,20 @@ describe('suspense', () => {
277321 const [ state , setState ] = useState ( false ) ;
278322 set = setState ;
279323 useEffect ( ( ) => {
324+ effectSetupSpy ( ) ;
280325 return ( ) => {
281326 effectSpy ( ) ;
282327 } ;
283- } , [ ] ) ;
328+ } , [ state ] ) ;
329+ useEffect ( effectWithoutCleanupSpy , [ state ] ) ;
284330
285331 useLayoutEffect ( ( ) => {
332+ layoutEffectSetupSpy ( ) ;
286333 return ( ) => {
287334 layoutEffectSpy ( ) ;
288335 } ;
289336 } , [ ] ) ;
337+ useLayoutEffect ( layoutEffectWithoutCleanupSpy , [ ] ) ;
290338
291339 return state ? (
292340 < div > { children } </ div >
@@ -305,19 +353,90 @@ describe('suspense', () => {
305353 </ Suspense > ,
306354 scratch
307355 ) ;
356+ expect ( layoutEffectSetupSpy ) . toHaveBeenCalledOnce ( ) ;
357+ expect ( layoutEffectWithoutCleanupSpy ) . toHaveBeenCalledOnce ( ) ;
308358
309359 set ( true ) ;
310360 rerender ( ) ;
311361 expect ( scratch . innerHTML ) . to . eql ( '<div>Suspended...</div>' ) ;
362+
363+ expect ( effectSetupSpy ) . toHaveBeenCalledOnce ( ) ;
364+ expect ( effectWithoutCleanupSpy ) . toHaveBeenCalledOnce ( ) ;
312365 expect ( effectSpy ) . toHaveBeenCalledOnce ( ) ;
313366 expect ( layoutEffectSpy ) . toHaveBeenCalledOnce ( ) ;
314367
315- return resolve ( ) . then ( ( ) => {
316- rerender ( ) ;
317- expect ( effectSpy ) . toHaveBeenCalledOnce ( ) ;
318- expect ( layoutEffectSpy ) . toHaveBeenCalledOnce ( ) ;
319- expect ( scratch . innerHTML ) . to . eql ( `<div><p>hi</p></div>` ) ;
320- } ) ;
368+ await resolve ( ) ;
369+ await act ( ( ) => rerender ( ) ) ;
370+ expect ( effectSpy ) . toHaveBeenCalledOnce ( ) ;
371+ expect ( layoutEffectSpy ) . toHaveBeenCalledOnce ( ) ;
372+ expect ( effectSetupSpy ) . toHaveBeenCalledTimes ( 2 ) ;
373+ expect ( effectWithoutCleanupSpy ) . toHaveBeenCalledTimes ( 2 ) ;
374+ expect ( layoutEffectSetupSpy ) . toHaveBeenCalledTimes ( 2 ) ;
375+ expect ( layoutEffectWithoutCleanupSpy ) . toHaveBeenCalledTimes ( 2 ) ;
376+ expect ( scratch . innerHTML ) . to . eql (
377+ `<div><div>Hello from LazyComp</div></div>`
378+ ) ;
379+ } ) ;
380+
381+ it ( 'should run a layout effect with changed deps once when revealed' , async ( ) => {
382+ let promise = Promise . resolve ( ) ;
383+ let set ;
384+ let first = true ;
385+ const setup = vi . fn ( ) ;
386+ const cleanup = vi . fn ( ) ;
387+ function App ( ) {
388+ const [ n , setN ] = useState ( 0 ) ;
389+ set = setN ;
390+ useLayoutEffect ( ( ) => {
391+ setup ( n ) ;
392+ return ( ) => cleanup ( n ) ;
393+ } , [ n ] ) ;
394+ if ( n && first ) {
395+ first = false ;
396+ throw promise ;
397+ }
398+ return < p > { n } </ p > ;
399+ }
400+
401+ render (
402+ < Suspense fallback = "loading" >
403+ < App />
404+ </ Suspense > ,
405+ scratch
406+ ) ;
407+ expect ( setup ) . toHaveBeenCalledTimes ( 1 ) ;
408+
409+ set ( 1 ) ;
410+ rerender ( ) ;
411+ expect ( scratch . textContent ) . to . equal ( 'loading' ) ;
412+ expect ( cleanup ) . toHaveBeenCalledTimes ( 1 ) ;
413+
414+ await promise ;
415+ await act ( ( ) => rerender ( ) ) ;
416+ expect ( scratch . textContent ) . to . equal ( '1' ) ;
417+ expect ( setup . mock . calls ) . to . deep . equal ( [ [ 0 ] , [ 1 ] ] ) ;
418+ expect ( cleanup ) . toHaveBeenCalledTimes ( 1 ) ;
419+ } ) ;
420+
421+ it ( 'should handle a promise thrown from a layout effect' , ( ) => {
422+ let threw = false ;
423+ function App ( ) {
424+ useLayoutEffect ( ( ) => {
425+ if ( ! threw ) {
426+ threw = true ;
427+ throw Promise . resolve ( ) ;
428+ }
429+ } ) ;
430+ return < p > x</ p > ;
431+ }
432+
433+ render (
434+ < Suspense fallback = "loading" >
435+ < App />
436+ </ Suspense > ,
437+ scratch
438+ ) ;
439+ rerender ( ) ;
321440 } ) ;
322441
323442 it ( 'should support a call to setState before rendering the fallback' , ( ) => {
0 commit comments