@@ -149,14 +149,15 @@ describe('React hydration', () => {
149149 </ QueryClientProvider > ,
150150 )
151151
152- // Existing observer should not have updated at this point,
153- // as that would indicate a side effect in the render phase
154- expect ( rendered . getByText ( 'string' ) ) . toBeInTheDocument ( )
152+ // The existing observer picks up the hydrated data once effects have
153+ // flushed, but not during the render phase (the aborted transition
154+ // test guards the render phase)
155+ expect ( rendered . getByText ( 'should change' ) ) . toBeInTheDocument ( )
155156 // New query data should be available immediately
156157 expect ( rendered . getByText ( 'added' ) ) . toBeInTheDocument ( )
157158
158159 await vi . advanceTimersByTimeAsync ( 0 )
159- // After effects phase has had time to run, the observer should have updated
160+ // Nothing changes after the effects phase has had time to run
160161 expect ( rendered . queryByText ( 'string' ) ) . not . toBeInTheDocument ( )
161162 expect ( rendered . getByText ( 'should change' ) ) . toBeInTheDocument ( )
162163
@@ -481,6 +482,116 @@ describe('React hydration', () => {
481482 clientQueryClient . clear ( )
482483 } )
483484
485+ it ( 'should not refetch an inactive query when hydrated data is fresh' , async ( ) => {
486+ const queryClient = new QueryClient ( )
487+ const queryFn = vi . fn ( ( ) => sleep ( 10 ) . then ( ( ) => 'client' ) )
488+
489+ function Page ( ) {
490+ const { data } = useQuery ( {
491+ queryKey : [ 'data' ] ,
492+ queryFn,
493+ staleTime : 1000 ,
494+ } )
495+ return < div > { data } </ div >
496+ }
497+
498+ // First visit fetches and caches the data
499+ const rendered = render (
500+ < QueryClientProvider client = { queryClient } >
501+ < Page />
502+ </ QueryClientProvider > ,
503+ )
504+ await vi . advanceTimersByTimeAsync ( 11 )
505+ expect ( rendered . getByText ( 'client' ) ) . toBeInTheDocument ( )
506+
507+ // Navigate away; the cached data goes stale while the page is unmounted
508+ rendered . rerender (
509+ < QueryClientProvider client = { queryClient } >
510+ < div />
511+ </ QueryClientProvider > ,
512+ )
513+ await vi . advanceTimersByTimeAsync ( 2000 )
514+
515+ // A loader fetches fresh data on the revisit and dehydrates it
516+ const loaderClient = new QueryClient ( )
517+ loaderClient . prefetchQuery ( {
518+ queryKey : [ 'data' ] ,
519+ queryFn : ( ) => sleep ( 10 ) . then ( ( ) => 'loader' ) ,
520+ } )
521+ await vi . advanceTimersByTimeAsync ( 10 )
522+ const dehydratedState = dehydrate ( loaderClient )
523+ loaderClient . clear ( )
524+
525+ queryFn . mockClear ( )
526+ rendered . rerender (
527+ < QueryClientProvider client = { queryClient } >
528+ < HydrationBoundary state = { dehydratedState } >
529+ < Page />
530+ </ HydrationBoundary >
531+ </ QueryClientProvider > ,
532+ )
533+
534+ // Hydration lands before the remounted useQuery subscribes, so the
535+ // fresh data is used as is instead of triggering a refetch
536+ expect ( rendered . getByText ( 'loader' ) ) . toBeInTheDocument ( )
537+ await vi . advanceTimersByTimeAsync ( 11 )
538+ expect ( queryFn ) . toHaveBeenCalledTimes ( 0 )
539+ expect ( rendered . getByText ( 'loader' ) ) . toBeInTheDocument ( )
540+
541+ queryClient . clear ( )
542+ } )
543+
544+ it ( 'should not refetch a query that remounts in the same commit as the boundary' , async ( ) => {
545+ const queryClient = new QueryClient ( )
546+ const queryFn = vi . fn ( ( ) => sleep ( 10 ) . then ( ( ) => 'client' ) )
547+
548+ function Page ( ) {
549+ const { data } = useQuery ( {
550+ queryKey : [ 'data' ] ,
551+ queryFn,
552+ staleTime : 1000 ,
553+ } )
554+ return < div > { data } </ div >
555+ }
556+
557+ const rendered = render (
558+ < QueryClientProvider client = { queryClient } >
559+ < Page />
560+ </ QueryClientProvider > ,
561+ )
562+ await vi . advanceTimersByTimeAsync ( 11 )
563+ expect ( rendered . getByText ( 'client' ) ) . toBeInTheDocument ( )
564+ await vi . advanceTimersByTimeAsync ( 2000 )
565+
566+ const loaderClient = new QueryClient ( )
567+ loaderClient . prefetchQuery ( {
568+ queryKey : [ 'data' ] ,
569+ queryFn : ( ) => sleep ( 10 ) . then ( ( ) => 'loader' ) ,
570+ } )
571+ await vi . advanceTimersByTimeAsync ( 10 )
572+ const dehydratedState = dehydrate ( loaderClient )
573+ loaderClient . clear ( )
574+
575+ queryFn . mockClear ( )
576+ // Wrapping the page in the boundary remounts it, so the old observer is
577+ // torn down and a new one subscribes in one go. The old subscription is
578+ // only cleaned up in the passive phase, so it is still on the query while
579+ // this commit's layout effects run
580+ rendered . rerender (
581+ < QueryClientProvider client = { queryClient } >
582+ < HydrationBoundary state = { dehydratedState } >
583+ < Page />
584+ </ HydrationBoundary >
585+ </ QueryClientProvider > ,
586+ )
587+
588+ expect ( rendered . getByText ( 'loader' ) ) . toBeInTheDocument ( )
589+ await vi . advanceTimersByTimeAsync ( 11 )
590+ expect ( queryFn ) . toHaveBeenCalledTimes ( 0 )
591+
592+ queryClient . clear ( )
593+ } )
594+
484595 it ( 'should not refetch when query has enabled set to false' , async ( ) => {
485596 const queryFn = vi . fn ( )
486597 const queryClient = new QueryClient ( )
0 commit comments