Skip to content
Closed
94 changes: 94 additions & 0 deletions packages/query-core/src/__tests__/queryObserver.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1494,4 +1494,98 @@ describe('queryObserver', () => {
unsubscribe2()
})
})

describe('hydration flag handling', () => {
test('should skip fetch when _pendingHydration flag is present', async () => {
const key = queryKey()
const queryFn = vi.fn().mockResolvedValue('data')

queryClient.setQueryData(key, 'initial-data')

const query = queryClient.getQueryCache().find({ queryKey: key })
expect(query).toBeDefined()
if (query) {
;(query as any)._pendingHydration = true
}

const observer = new QueryObserver(queryClient, {
queryKey: key,
queryFn,
staleTime: 0,
})

const unsubscribe = observer.subscribe(() => undefined)
await vi.advanceTimersByTimeAsync(0)

expect(queryFn).toHaveBeenCalledTimes(0)

expect(observer.getCurrentResult()).toMatchObject({
status: 'success',
fetchStatus: 'idle',
data: 'initial-data',
})

expect((query as any)?._pendingHydration).toBe(true)

unsubscribe()
})

test('should still fetch when _pendingHydration flag is present but refetchOnMount is "always"', async () => {
const key = queryKey()
const queryFn = vi.fn().mockResolvedValue('data')

queryClient.setQueryData(key, 'initial-data')

const query = queryClient.getQueryCache().find({ queryKey: key })
expect(query).toBeDefined()
if (query) {
;(query as any)._pendingHydration = true
}

const observer = new QueryObserver(queryClient, {
queryKey: key,
queryFn,
staleTime: 0,
refetchOnMount: 'always',
})

const unsubscribe = observer.subscribe(() => undefined)
await vi.advanceTimersByTimeAsync(0)

expect(queryFn).toHaveBeenCalledTimes(1)

unsubscribe()
})

test('should handle refetchOnMount as a function returning "always" with hydration flag', async () => {
const key = queryKey()
const queryFn = vi.fn().mockResolvedValue('data')
const refetchOnMountFn = vi.fn(() => 'always' as const)

queryClient.setQueryData(key, 'initial-data')

const query = queryClient.getQueryCache().find({ queryKey: key })
expect(query).toBeDefined()
if (query) {
;(query as any)._pendingHydration = true
}

const observer = new QueryObserver(queryClient, {
queryKey: key,
queryFn,
staleTime: 0,
refetchOnMount: refetchOnMountFn,
})

const unsubscribe = observer.subscribe(() => undefined)
await vi.advanceTimersByTimeAsync(0)

expect(refetchOnMountFn).toHaveBeenCalledTimes(1)
expect(refetchOnMountFn).toHaveBeenCalledWith(query)

expect(queryFn).toHaveBeenCalledTimes(1)

unsubscribe()
})
})
})
23 changes: 22 additions & 1 deletion packages/query-core/src/queryObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,28 @@ export class QueryObserver<
if (this.listeners.size === 1) {
this.#currentQuery.addObserver(this)

if (shouldFetchOnMount(this.#currentQuery, this.options)) {
// Check hydration flag but DON'T delete it here.
// The flag is cleared in HydrationBoundary's effect after hydrate().
const hasPendingHydration = !!(this.#currentQuery as any)
._pendingHydration

const resolvedRefetchOnMount =
typeof this.options.refetchOnMount === 'function'
? this.options.refetchOnMount(this.#currentQuery)
: this.options.refetchOnMount

const localOptions = {
...this.options,
refetchOnMount: resolvedRefetchOnMount,
}

const shouldSkipFetch =
hasPendingHydration && resolvedRefetchOnMount !== 'always'

if (
shouldFetchOnMount(this.#currentQuery, localOptions) &&
!shouldSkipFetch
) {
this.#executeFetch()
} else {
this.updateResult()
Expand Down
14 changes: 14 additions & 0 deletions packages/react-query/src/HydrationBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ export const HydrationBoundary = ({
hydrate(client, { queries: newQueries }, optionsRef.current)
}
if (existingQueries.length > 0) {
existingQueries.forEach((q) => {
const query = queryCache.get(q.queryHash)
if (query) {
// Temporary flag to prevent double-fetching during hydration
// Will be immediately removed in HydrationBoundary useEffect
;(query as any)._pendingHydration = true
}
})
return existingQueries
}
}
Expand All @@ -101,6 +109,12 @@ export const HydrationBoundary = ({
React.useEffect(() => {
if (hydrationQueue) {
hydrate(client, { queries: hydrationQueue }, optionsRef.current)
hydrationQueue.forEach((q) => {
const query = client.getQueryCache().get(q.queryHash)
if (query) {
delete (query as any)._pendingHydration
}
})
}
}, [client, hydrationQueue])

Expand Down
Loading
Loading