diff --git a/.changeset/quiet-timers-rest.md b/.changeset/quiet-timers-rest.md new file mode 100644 index 00000000000..389014363ca --- /dev/null +++ b/.changeset/quiet-timers-rest.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-core': patch +--- + +Do not schedule garbage collection on the server. A timer scheduled during server rendering captures the async context it was created in and keeps that whole render alive until it fires, while the client it would clean up is dropped with the response. Only queries and mutations with an explicit finite `gcTime` were affected, since the server default is already `Infinity`. diff --git a/packages/query-core/src/__tests__/query.test.tsx b/packages/query-core/src/__tests__/query.test.tsx index 5ac3f20663b..54f3ec37d9f 100644 --- a/packages/query-core/src/__tests__/query.test.tsx +++ b/packages/query-core/src/__tests__/query.test.tsx @@ -12,6 +12,7 @@ import { QueryObserver, dehydrate, hydrate, + timeoutManager, } from '..' import { hashQueryKeyByOptions } from '../utils' import { mockOnlineManagerIsOnline, setIsServer } from './utils' @@ -983,6 +984,25 @@ describe('query', () => { } }) + it('should not schedule garbage collection on the server, even with an explicit gcTime', () => { + const resetIsServer = setIsServer(true) + const scheduled = vi.spyOn(timeoutManager, 'setTimeout') + + try { + const query = queryCache.build(queryClient, { + queryKey: queryKey(), + queryFn: () => 'data', + gcTime: 1000, + }) + + expect(query.gcTime).toBe(1000) + expect(scheduled).not.toHaveBeenCalled() + } finally { + scheduled.mockRestore() + resetIsServer() + } + }) + it('constructor should call initialDataUpdatedAt if defined as a function', async () => { const key = queryKey() diff --git a/packages/query-core/src/removable.ts b/packages/query-core/src/removable.ts index 4e84ee62060..c63d5aee292 100644 --- a/packages/query-core/src/removable.ts +++ b/packages/query-core/src/removable.ts @@ -14,6 +14,10 @@ export abstract class Removable { protected scheduleGc(): void { this.clearGcTimeout() + if (isServerEnvironment()) { + return + } + if (isValidTimeout(this.gcTime)) { this.#gcTimeout = timeoutManager.setTimeout(() => { this.optionalRemove()