From af4cc96d962d8bec70bd47ffed8a5b86f9bdd564 Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Fri, 13 Mar 2026 10:45:56 -0700 Subject: [PATCH] feat(query-core): add refetchIntervalOnServer option Add a new `refetchIntervalOnServer` option to `QueryObserverOptions` that allows `refetchInterval` to remain active in server environments. By default, `refetchInterval` is disabled when `isServer` is true (i.e. `typeof window === 'undefined'`), which makes sense for SSR scenarios. However, long-running server processes like daemons and background workers that use TanStack Query for data synchronization need polling to function. This option follows the same pattern as `refetchIntervalInBackground` -- a per-query boolean that defaults to `false`, preserving existing behavior. Co-Authored-By: Claude Opus 4.6 --- .../src/__tests__/queryObserver.test.tsx | 52 +++++++++++++++++++ packages/query-core/src/queryObserver.ts | 2 +- packages/query-core/src/types.ts | 12 +++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/packages/query-core/src/__tests__/queryObserver.test.tsx b/packages/query-core/src/__tests__/queryObserver.test.tsx index 689dd8d2e19..566739ffc14 100644 --- a/packages/query-core/src/__tests__/queryObserver.test.tsx +++ b/packages/query-core/src/__tests__/queryObserver.test.tsx @@ -10,6 +10,7 @@ import { } from 'vitest' import { queryKey, sleep } from '@tanstack/query-test-utils' import { QueryClient, QueryObserver, focusManager } from '..' +import { setIsServer } from './utils' import type { QueryObserverResult } from '..' describe('queryObserver', () => { @@ -867,6 +868,57 @@ describe('queryObserver', () => { focusManager.setFocused(true) }) + test('should not refetch on server by default', async () => { + const restoreIsServer = setIsServer(true) + + const key = queryKey() + let count = 0 + + const observer = new QueryObserver(queryClient, { + queryKey: key, + queryFn: () => { + count++ + return Promise.resolve('data') + }, + refetchInterval: 10, + }) + + const unsubscribe = observer.subscribe(() => undefined) + await vi.advanceTimersByTimeAsync(30) + + // Should only have the initial fetch, no refetch interval + expect(count).toBe(1) + + unsubscribe() + restoreIsServer() + }) + + test('should refetch on server when refetchIntervalOnServer is true', async () => { + const restoreIsServer = setIsServer(true) + + const key = queryKey() + let count = 0 + + const observer = new QueryObserver(queryClient, { + queryKey: key, + queryFn: () => { + count++ + return Promise.resolve('data') + }, + refetchInterval: 10, + refetchIntervalOnServer: true, + }) + + const unsubscribe = observer.subscribe(() => undefined) + await vi.advanceTimersByTimeAsync(30) + + // Should have the initial fetch plus refetches from the interval + expect(count).toBeGreaterThan(1) + + unsubscribe() + restoreIsServer() + }) + test('should not use replaceEqualDeep for select value when structuralSharing option is true', async () => { const key = queryKey() diff --git a/packages/query-core/src/queryObserver.ts b/packages/query-core/src/queryObserver.ts index 463407a0737..d0fee91d23f 100644 --- a/packages/query-core/src/queryObserver.ts +++ b/packages/query-core/src/queryObserver.ts @@ -389,7 +389,7 @@ export class QueryObserver< this.#currentRefetchInterval = nextInterval if ( - isServer || + (isServer && !this.options.refetchIntervalOnServer) || resolveEnabled(this.options.enabled, this.#currentQuery) === false || !isValidTimeout(this.#currentRefetchInterval) || this.#currentRefetchInterval === 0 diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index 4f3f4caed20..cc67b527542 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -350,6 +350,18 @@ export interface QueryObserverOptions< * Defaults to `false`. */ refetchIntervalInBackground?: boolean + /** + * If set to `true`, the refetch interval will be active even in server environments + * (where `typeof window === 'undefined'`). + * + * By default, `refetchInterval` is paused on the server because most server-side + * rendering scenarios don't need polling. However, long-running server processes + * (like daemons or background workers) that use TanStack Query for data synchronization + * may need polling to function correctly. + * + * Defaults to `false`. + */ + refetchIntervalOnServer?: boolean /** * If set to `true`, the query will refetch on window focus if the data is stale. * If set to `false`, the query will not refetch on window focus.