Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions .changeset/release-settled-retryer.md
Original file line number Diff line number Diff line change
@@ -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).
37 changes: 31 additions & 6 deletions packages/query-core/src/__tests__/query.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,15 @@ import {
import {
CancelledError,
Query,
QueryCache,
QueryClient,
QueryObserver,
dehydrate,
hydrate,
} 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
Expand Down Expand Up @@ -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<unknown> | 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()

Expand Down
11 changes: 8 additions & 3 deletions packages/query-core/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TData>
| undefined,
Expand All @@ -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) {
Expand Down Expand Up @@ -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()
}
Expand Down