From d504d4ddee5e5d190c373bc8b0038b76e5ffe959 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Sun, 9 Aug 2026 09:51:16 -0700 Subject: [PATCH] fix(query-core): release the retryer once a fetch settles A query kept its last retryer for life, and the settled promise holds that fetch's raw result: a second full copy of the data once a refetch has been structurally shared into state.data, and still held after reset() or removal. --- .changeset/release-settled-retryer.md | 5 +++ .../query-core/src/__tests__/query.test.tsx | 37 ++++++++++++++++--- packages/query-core/src/query.ts | 11 ++++-- 3 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 .changeset/release-settled-retryer.md diff --git a/.changeset/release-settled-retryer.md b/.changeset/release-settled-retryer.md new file mode 100644 index 00000000000..84eb00cd247 --- /dev/null +++ b/.changeset/release-settled-retryer.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-core': patch +--- + +Release a query's retryer once its fetch settles, so the settled promise no longer keeps that fetch's raw result in memory alongside the structurally shared `state.data` (or after the query is reset or removed). diff --git a/packages/query-core/src/__tests__/query.test.tsx b/packages/query-core/src/__tests__/query.test.tsx index fdac03e3987..f4edc0d0c10 100644 --- a/packages/query-core/src/__tests__/query.test.tsx +++ b/packages/query-core/src/__tests__/query.test.tsx @@ -7,6 +7,7 @@ import { import { CancelledError, Query, + QueryCache, QueryClient, QueryObserver, dehydrate, @@ -14,12 +15,7 @@ import { } from '..' import { hashQueryKeyByOptions } from '../utils' import { mockOnlineManagerIsOnline, setIsServer } from './utils' -import type { - QueryCache, - QueryFunctionContext, - QueryKey, - QueryObserverResult, -} from '..' +import type { QueryFunctionContext, QueryKey, QueryObserverResult } from '..' describe('query', () => { let queryClient: QueryClient @@ -543,6 +539,35 @@ describe('query', () => { expect(query.state.error).not.toBeInstanceOf(CancelledError) }) + it('should release the retryer once its fetch has settled', async () => { + const key = queryKey() + let refetch: Promise | undefined + const testCache = new QueryCache({ + onSuccess: (_data, query) => { + refetch ??= query.fetch() + }, + }) + const testClient = new QueryClient({ queryCache: testCache }) + + const prefetch = testClient.prefetchQuery({ + queryKey: key, + queryFn: () => sleep(10).then(() => 'data'), + }) + const query = testCache.find({ queryKey: key })! + const firstPromise = query.promise + expect(firstPromise).toBeDefined() + + await vi.advanceTimersByTimeAsync(10) + await prefetch + expect(query.promise).toBeDefined() + expect(query.promise).not.toBe(firstPromise) + + await vi.advanceTimersByTimeAsync(10) + await refetch + expect(query.state.data).toBe('data') + expect(query.promise).toBeUndefined() + }) + it('the previous query status should be kept when refetching', async () => { const key = queryKey() diff --git a/packages/query-core/src/query.ts b/packages/query-core/src/query.ts index 62bc9a16082..70e0fd9a0e7 100644 --- a/packages/query-core/src/query.ts +++ b/packages/query-core/src/query.ts @@ -533,7 +533,7 @@ export class Query< } // Try to fetch the data - this.#retryer = createRetryer({ + const retryer = (this.#retryer = createRetryer({ initialPromise: fetchOptions?.initialPromise as | Promise | undefined, @@ -560,10 +560,10 @@ export class Query< retryDelay: context.options.retryDelay, networkMode: context.options.networkMode, canRun: () => true, - }) + })) try { - const data = await this.#retryer.start() + const data = await retryer.start() // this is more of a runtime guard // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (data === undefined) { @@ -619,6 +619,11 @@ export class Query< throw error // rethrow the error for further handling } finally { + // The settled retryer's promise would otherwise pin this fetch's raw + // result (a second copy after structural sharing) for the query's lifetime + if (this.#retryer === retryer) { + this.#retryer = undefined + } // Schedule query gc after fetching this.scheduleGc() }