diff --git a/packages/query-core/src/__tests__/queryClient.test.tsx b/packages/query-core/src/__tests__/queryClient.test.tsx index c09db304467..fcb17eecf3f 100644 --- a/packages/query-core/src/__tests__/queryClient.test.tsx +++ b/packages/query-core/src/__tests__/queryClient.test.tsx @@ -1692,6 +1692,77 @@ describe('queryClient', () => { expect(queryFn2).toHaveBeenCalledTimes(0) expect(didSkipTokenRun).toBe(false) }) + + it('should refetch queries selected via a state-based predicate', async () => { + // Regression test for #10705 — when `resetQueries` is called with a + // predicate keyed on `query.state.status`, reset() mutates the status + // before refetchQueries re-evaluates the predicate, so the matched set + // collapses and no refetch happens. + const erroredKey = queryKey() + const successKey = queryKey() + const erroredQueryFn = vi + .fn<(...args: Array) => Promise>() + .mockRejectedValue(new Error('boom')) + const successQueryFn = vi + .fn<(...args: Array) => string>() + .mockReturnValue('ok') + + // Seed the queries directly (fetchQuery returns a Promise — works under + // vi.useFakeTimers because no setTimeout is involved). + await queryClient + .fetchQuery({ + queryKey: erroredKey, + queryFn: erroredQueryFn, + retry: false, + }) + .catch(() => undefined) + await queryClient.fetchQuery({ + queryKey: successKey, + queryFn: successQueryFn, + }) + expect(queryClient.getQueryState(erroredKey)?.status).toBe('error') + expect(queryClient.getQueryState(successKey)?.status).toBe('success') + + // Mount observers so the queries are "active" — refetch only runs for + // queries with observers. The observers may also auto-refetch on mount + // because the data is stale; the test re-resets the mocks AFTER that + // settles so the only thing the assertion counts is the resetQueries + // refetch under test. + const erroredObserver = new QueryObserver(queryClient, { + queryKey: erroredKey, + queryFn: erroredQueryFn, + retry: false, + }) + const successObserver = new QueryObserver(queryClient, { + queryKey: successKey, + queryFn: successQueryFn, + }) + erroredObserver.subscribe(() => undefined) + successObserver.subscribe(() => undefined) + + // Let any mount-time fetches settle so the errored query is back in + // 'error' state when we call resetQueries. + await vi.runOnlyPendingTimersAsync() + + erroredQueryFn.mockClear() + successQueryFn.mockClear() + + expect(queryClient.getQueryState(erroredKey)?.status).toBe('error') + + await queryClient + .resetQueries({ + predicate: (query) => query.state.status === 'error', + }) + .catch(() => undefined) + + // The errored query was reset AND re-fetched. + expect(erroredQueryFn).toHaveBeenCalledTimes(1) + // The successful query stayed untouched. + expect(successQueryFn).toHaveBeenCalledTimes(0) + + successObserver.destroy() + erroredObserver.destroy() + }) }) describe('focusManager and onlineManager', () => { diff --git a/packages/query-core/src/queryClient.ts b/packages/query-core/src/queryClient.ts index d82106c7375..499eb136319 100644 --- a/packages/query-core/src/queryClient.ts +++ b/packages/query-core/src/queryClient.ts @@ -260,13 +260,20 @@ export class QueryClient { const queryCache = this.#queryCache return notifyManager.batch(() => { - queryCache.findAll(filters).forEach((query) => { + // Snapshot matched queries BEFORE calling reset, because reset() mutates + // each query's state (e.g. status flips from "error"/"success" to + // "pending"). Re-running the same state-based predicate afterwards would + // no longer match the same set, so refetch would skip the queries we + // just reset. Match by identity instead — query references are stable + // across reset. + const matchedQueries = queryCache.findAll(filters) + matchedQueries.forEach((query) => { query.reset() }) return this.refetchQueries( { type: 'active', - ...filters, + predicate: (query) => matchedQueries.includes(query), }, options, )