Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/moody-cities-stand.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/react-query': patch
'@tanstack/query-core': patch
---

prevent registered useQueries from skipping hydration
9 changes: 7 additions & 2 deletions packages/query-core/src/hydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,11 @@ export function hydrate(
let query = queryCache.get(queryHash)
const existingQueryIsPending = query?.state.status === 'pending'
const existingQueryIsFetching = query?.state.fetchStatus === 'fetching'
const existingQueryIsUndefinedOrIsIdleUseQuery =
!query ||
(query.state.dataUpdatedAt === 0 &&
query.state.status === 'pending' &&
query.state.fetchStatus === 'idle')

// Do not hydrate if an existing query exists with newer data
if (query) {
Expand Down Expand Up @@ -262,8 +267,8 @@ export function hydrate(

if (
promise &&
!existingQueryIsPending &&
!existingQueryIsFetching &&
(existingQueryIsUndefinedOrIsIdleUseQuery ||
(!existingQueryIsPending && !existingQueryIsFetching)) &&
// Only hydrate if dehydration is newer than any existing data,
// this is always true for new queries
(dehydratedAt === undefined || dehydratedAt > query.state.dataUpdatedAt)
Expand Down
6 changes: 6 additions & 0 deletions packages/react-query/src/HydrationBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,15 @@ export const HydrationBoundary = ({
const existingQueries: DehydratedState['queries'] = []
for (const dehydratedQuery of queries) {
const existingQuery = queryCache.get(dehydratedQuery.queryHash)
const existingQueryIsIdleUseQuery =
existingQuery?.state.dataUpdatedAt === 0 &&
existingQuery.state.status === 'pending' &&
existingQuery.state.fetchStatus === 'idle'

if (!existingQuery) {
newQueries.push(dehydratedQuery)
} else if (existingQueryIsIdleUseQuery) {
newQueries.push(dehydratedQuery)
} else {
const hydrationIsNewer =
dehydratedQuery.state.dataUpdatedAt >
Expand Down
66 changes: 66 additions & 0 deletions packages/react-query/src/__tests__/HydrationBoundary.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
HydrationBoundary,
QueryClient,
QueryClientProvider,
defaultShouldDehydrateQuery,
dehydrate,
useQuery,
useSuspenseQuery,
} from '..'
import type { hydrate } from '@tanstack/query-core'

Expand Down Expand Up @@ -481,6 +483,70 @@ describe('React hydration', () => {
clientQueryClient.clear()
})

test('should hydrate pending idle queries in render to avoid suspense refetches', async () => {
const queryKey = ['string'] as const

const makeQueryClient = () =>
new QueryClient({
defaultOptions: {
dehydrate: {
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) ||
query.state.status === 'pending',
shouldRedactErrors: () => false,
},
},
})

const prefetchClient = makeQueryClient()
void prefetchClient.prefetchQuery({
queryKey,
queryFn: () => Promise.resolve(['stringCached']),
staleTime: Infinity,
})
const dehydratedState = dehydrate(prefetchClient)

const queryFn = vi.fn(() => Promise.resolve(['string']))
const suspenseQueryFn = vi.fn(() => Promise.resolve(['string']))
const queryClient = new QueryClient()

function Header() {
useQuery({
queryKey,
queryFn,
})
return null
}

function Page() {
const { data } = useSuspenseQuery({
queryKey,
queryFn: suspenseQueryFn,
})
return <div>{data}</div>
}

render(
<QueryClientProvider client={queryClient}>
<Header />
<HydrationBoundary state={dehydratedState}>
<React.Suspense fallback="loading">
<Page />
</React.Suspense>
</HydrationBoundary>
</QueryClientProvider>,
)

await Promise.resolve()
await Promise.resolve()
await Promise.resolve()
await vi.advanceTimersByTimeAsync(1)
expect(queryClient.getQueryData(queryKey)).toEqual(['stringCached'])
expect(suspenseQueryFn).toHaveBeenCalledTimes(0)

queryClient.clear()
})

test('should not refetch when query has enabled set to false', async () => {
const queryFn = vi.fn()
const queryClient = new QueryClient()
Expand Down
Loading