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