@@ -167,13 +167,15 @@ describe('Preact hydration', () => {
167167 queryClient . clear ( )
168168 } )
169169
170- // When we hydrate in transitions that are later aborted, it could be
171- // confusing to both developers and users if we suddenly updated existing
172- // state on the screen (why did this update when it was not stale, nothing
173- // remounted, I didn't change tabs etc?).
174- // Any queries that does not exist in the cache yet can still be hydrated
175- // since they don't have any observers on the current page that would update.
176- it ( 'should hydrate new but not existing queries if transition is aborted' , async ( ) => {
170+ // Preact has no equivalent of a render that never happened. A tree that
171+ // suspends is diffed and committed, layout effects and all, and only then
172+ // gets swapped out for the fallback. What keeps the current page intact is
173+ // that queries with observers wait for a passive effect, and those never
174+ // run for a tree that suspended, see the test further down with a sidebar
175+ // that survives the transition. Here the rerender introduces a new root,
176+ // which remounts everything and unsubscribes the observer before the
177+ // boundary even renders, so by then this is a query nobody is watching.
178+ it ( 'should hydrate an unobserved query even if the render that carried it suspends' , async ( ) => {
177179 const initialDehydratedState = JSON . parse ( stringifiedState )
178180 const queryClient = new QueryClient ( )
179181
@@ -235,6 +237,11 @@ describe('Preact hydration', () => {
235237 )
236238
237239 expect ( rendered . getByText ( 'loading' ) ) . toBeInTheDocument ( )
240+ // The tree committed before the fallback took over, so its data is in
241+ // the cache even though none of it made it to the screen
242+ expect ( queryClient . getQueryData ( stringKey ) ) . toEqual ( [
243+ 'should not change' ,
244+ ] )
238245 } )
239246
240247 startTransition ( ( ) => {
@@ -247,22 +254,16 @@ describe('Preact hydration', () => {
247254 </ QueryClientProvider > ,
248255 )
249256
250- // This query existed before the transition so it should stay the same
251- expect ( rendered . getByText ( stringKey [ 0 ] ! ) ) . toBeInTheDocument ( )
252- expect (
253- rendered . queryByText ( 'should not change' ) ,
254- ) . not . toBeInTheDocument ( )
257+ // Both pages render what the cache holds, the query that already
258+ // existed included
259+ expect ( rendered . getByText ( 'should not change' ) ) . toBeInTheDocument ( )
260+ expect ( rendered . queryByText ( stringKey [ 0 ] ! ) ) . not . toBeInTheDocument ( )
255261 // New query data should be available immediately because it was
256262 // hydrated in the previous transition, even though the new dehydrated
257263 // state did not contain it
258264 expect ( rendered . getByText ( addedKey [ 0 ] ! ) ) . toBeInTheDocument ( )
259265 } )
260266
261- await vi . advanceTimersByTimeAsync ( 20 )
262- // It should stay the same even after effects have had a chance to run
263- expect ( rendered . getByText ( stringKey [ 0 ] ! ) ) . toBeInTheDocument ( )
264- expect ( rendered . queryByText ( 'should not change' ) ) . not . toBeInTheDocument ( )
265-
266267 queryClient . clear ( )
267268 } )
268269
@@ -311,6 +312,122 @@ describe('Preact hydration', () => {
311312 } )
312313 } )
313314
315+ it ( 'should not refetch an inactive query when hydrated data is fresh' , async ( ) => {
316+ const key = queryKey ( )
317+ const queryClient = new QueryClient ( )
318+ const queryFn = vi . fn ( ( ) => sleep ( 10 ) . then ( ( ) => 'client' ) )
319+
320+ function Page ( ) {
321+ const { data } = useQuery ( {
322+ queryKey : key ,
323+ queryFn,
324+ staleTime : 1000 ,
325+ } )
326+ return < div > { data } </ div >
327+ }
328+
329+ // First visit fetches and caches the data
330+ const rendered = render (
331+ < QueryClientProvider client = { queryClient } >
332+ < Page />
333+ </ QueryClientProvider > ,
334+ )
335+ await vi . advanceTimersByTimeAsync ( 11 )
336+ expect ( rendered . getByText ( 'client' ) ) . toBeInTheDocument ( )
337+
338+ // Navigate away, the cached data goes stale while the page is unmounted
339+ rendered . rerender (
340+ < QueryClientProvider client = { queryClient } >
341+ < div />
342+ </ QueryClientProvider > ,
343+ )
344+ await vi . advanceTimersByTimeAsync ( 2000 )
345+
346+ // A loader fetches fresh data for the revisit and dehydrates it
347+ const loaderClient = new QueryClient ( )
348+ loaderClient . prefetchQuery ( {
349+ queryKey : key ,
350+ queryFn : ( ) => sleep ( 10 ) . then ( ( ) => 'loader' ) ,
351+ } )
352+ await vi . advanceTimersByTimeAsync ( 10 )
353+ const dehydratedState = dehydrate ( loaderClient )
354+ loaderClient . clear ( )
355+
356+ queryFn . mockClear ( )
357+ rendered . rerender (
358+ < QueryClientProvider client = { queryClient } >
359+ < HydrationBoundary state = { dehydratedState } >
360+ < Page />
361+ </ HydrationBoundary >
362+ </ QueryClientProvider > ,
363+ )
364+
365+ // Hydration lands before the remounted useQuery subscribes, so it uses the
366+ // fresh data instead of fetching it all over again
367+ await vi . advanceTimersByTimeAsync ( 11 )
368+ expect ( queryFn ) . toHaveBeenCalledTimes ( 0 )
369+ expect ( rendered . getByText ( 'loader' ) ) . toBeInTheDocument ( )
370+
371+ queryClient . clear ( )
372+ } )
373+
374+ it ( 'should not hydrate a query that is on screen while a sibling suspends' , async ( ) => {
375+ const key = queryKey ( )
376+ const queryClient = new QueryClient ( )
377+
378+ function Sidebar ( ) {
379+ const { data } = useQuery ( {
380+ queryKey : key ,
381+ queryFn : ( ) => sleep ( 10 ) . then ( ( ) => 'sidebar' ) ,
382+ staleTime : Infinity ,
383+ } )
384+ return < div > { data } </ div >
385+ }
386+
387+ function Thrower ( ) : never {
388+ throw new Promise ( ( ) => {
389+ // Never resolve
390+ } )
391+ }
392+
393+ const rendered = render (
394+ < QueryClientProvider client = { queryClient } >
395+ < Sidebar />
396+ </ QueryClientProvider > ,
397+ )
398+ await vi . advanceTimersByTimeAsync ( 11 )
399+ expect ( rendered . getByText ( 'sidebar' ) ) . toBeInTheDocument ( )
400+
401+ const loaderClient = new QueryClient ( )
402+ loaderClient . prefetchQuery ( {
403+ queryKey : key ,
404+ queryFn : ( ) => sleep ( 10 ) . then ( ( ) => 'loader' ) ,
405+ } )
406+ await vi . advanceTimersByTimeAsync ( 10 )
407+ const dehydratedState = dehydrate ( loaderClient )
408+ loaderClient . clear ( )
409+
410+ // The route the app is navigating to suspends and never gets there, while
411+ // the sidebar stays mounted and keeps rendering the query
412+ rendered . rerender (
413+ < QueryClientProvider client = { queryClient } >
414+ < Sidebar />
415+ < Suspense fallback = "loading" >
416+ < HydrationBoundary state = { dehydratedState } >
417+ < Thrower />
418+ </ HydrationBoundary >
419+ </ Suspense >
420+ </ QueryClientProvider > ,
421+ )
422+
423+ expect ( rendered . getByText ( 'loading' ) ) . toBeInTheDocument ( )
424+ await vi . advanceTimersByTimeAsync ( 100 )
425+ expect ( rendered . getByText ( 'sidebar' ) ) . toBeInTheDocument ( )
426+ expect ( queryClient . getQueryData ( key ) ) . toBe ( 'sidebar' )
427+
428+ queryClient . clear ( )
429+ } )
430+
314431 it ( 'should not hydrate queries if state is null' , async ( ) => {
315432 const queryClient = new QueryClient ( )
316433
0 commit comments