Skip to content

Commit 7329a35

Browse files
committed
fix(react-query): hydrate disabled query shells in HydrationBoundary
1 parent a3ec7b3 commit 7329a35

4 files changed

Lines changed: 112 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/react-query': patch
3+
---
4+
5+
Fix `HydrationBoundary` deferring hydration for disabled query shells

packages/query-core/src/__tests__/hydration.test.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
22
import { queryKey, sleep } from '@tanstack/query-test-utils'
33
import { QueryClient } from '../queryClient'
44
import { QueryCache } from '../queryCache'
5+
import { QueryObserver } from '../queryObserver'
56
import { dehydrate, hydrate } from '../hydration'
67
import { MutationCache } from '../mutationCache'
78
import { executeMutation, mockOnlineManagerIsOnline } from './utils'
@@ -960,6 +961,47 @@ describe('dehydration and rehydration', () => {
960961
})
961962
})
962963

964+
test('should hydrate over an existing disabled query shell', async () => {
965+
const key = queryKey()
966+
967+
const prefetchClient = new QueryClient()
968+
const prefetchPromise = prefetchClient.prefetchQuery({
969+
queryKey: key,
970+
queryFn: () => sleep(10).then(() => 'prefetched'),
971+
})
972+
await vi.advanceTimersByTimeAsync(10)
973+
await prefetchPromise
974+
const dehydrated = dehydrate(prefetchClient)
975+
976+
const hydrationClient = new QueryClient()
977+
const observer = new QueryObserver(hydrationClient, {
978+
queryKey: key,
979+
queryFn: () => Promise.resolve('client'),
980+
enabled: false,
981+
})
982+
const unsubscribe = observer.subscribe(vi.fn())
983+
984+
const disabledShellQuery = hydrationClient.getQueryCache().find({ queryKey: key })
985+
986+
expect(disabledShellQuery?.state).toMatchObject({
987+
data: undefined,
988+
fetchStatus: 'idle',
989+
status: 'pending',
990+
})
991+
expect(disabledShellQuery?.isDisabled()).toBe(true)
992+
993+
hydrate(hydrationClient, dehydrated)
994+
995+
expect(hydrationClient.getQueryCache().find({ queryKey: key })?.state).toMatchObject({
996+
data: 'prefetched',
997+
status: 'success',
998+
})
999+
1000+
unsubscribe()
1001+
prefetchClient.clear()
1002+
hydrationClient.clear()
1003+
})
1004+
9631005
test('should transform promise result', async () => {
9641006
const key = queryKey()
9651007
const queryClient = new QueryClient({

packages/react-query/src/HydrationBoundary.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,17 @@ export const HydrationBoundary = ({
8282
dehydratedQuery.dehydratedAt >
8383
existingQuery.state.dataUpdatedAt)
8484

85+
const canHydrateExistingInRender =
86+
existingQuery.state.fetchStatus === 'idle' &&
87+
(existingQuery.state.data === undefined ||
88+
existingQuery.isDisabled())
89+
8590
if (hydrationIsNewer) {
86-
existingQueries.push(dehydratedQuery)
91+
if (canHydrateExistingInRender) {
92+
newQueries.push(dehydratedQuery)
93+
} else {
94+
existingQueries.push(dehydratedQuery)
95+
}
8796
}
8897
}
8998
}

packages/react-query/src/__tests__/HydrationBoundary.test.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import {
99
QueryClientProvider,
1010
dehydrate,
1111
useQuery,
12+
useSuspenseQuery,
1213
} from '..'
14+
import { renderWithClient } from './utils'
1315
import type { hydrate } from '@tanstack/query-core'
1416

1517
describe('React hydration', () => {
@@ -511,6 +513,59 @@ describe('React hydration', () => {
511513
queryClient.clear()
512514
})
513515

516+
test('should hydrate an existing disabled query before a nested suspense query with the same key mounts', async () => {
517+
const prefetchedData = ['prefetched']
518+
const clientQueryFn = vi.fn(() => sleep(20).then(() => ['client']))
519+
const queryClient = new QueryClient()
520+
521+
const prefetchClient = new QueryClient()
522+
const prefetchPromise = prefetchClient.prefetchQuery({
523+
queryKey: ['string'],
524+
queryFn: () => sleep(10).then(() => prefetchedData),
525+
})
526+
await vi.advanceTimersByTimeAsync(10)
527+
await prefetchPromise
528+
const dehydratedState = dehydrate(prefetchClient)
529+
prefetchClient.clear()
530+
531+
function Header() {
532+
useQuery({
533+
queryKey: ['string'],
534+
queryFn: clientQueryFn,
535+
enabled: false,
536+
})
537+
return null
538+
}
539+
540+
function Child() {
541+
useSuspenseQuery({
542+
queryKey: ['string'],
543+
queryFn: clientQueryFn,
544+
})
545+
return null
546+
}
547+
548+
renderWithClient(
549+
queryClient,
550+
<>
551+
<Header />
552+
<React.Suspense fallback="loading">
553+
<HydrationBoundary state={dehydratedState}>
554+
<Child />
555+
</HydrationBoundary>
556+
</React.Suspense>
557+
</>,
558+
)
559+
560+
expect(queryClient.getQueryCache().find({ queryKey: ['string'] })?.state).toMatchObject({
561+
data: prefetchedData,
562+
status: 'success',
563+
})
564+
expect(clientQueryFn).toHaveBeenCalledTimes(0)
565+
566+
queryClient.clear()
567+
})
568+
514569
test('should not refetch when query has staleTime set to Infinity', async () => {
515570
const queryFn = vi.fn()
516571
const queryClient = new QueryClient()

0 commit comments

Comments
 (0)