From 6bba5b44eb2c356ac08fe23495856458611c0ede Mon Sep 17 00:00:00 2001 From: makoto-developer <72484465+makoto-developer@users.noreply.github.com> Date: Sun, 26 Jul 2026 11:20:04 +0900 Subject: [PATCH] fix(query-core): keep retrying while unfocused when refetchIntervalInBackground is true The retryer's canContinue gate required focusManager.isFocused(), so a failed background refetch paused until the tab regained focus even with refetchIntervalInBackground set, defeating that option. Thread refetchIntervalInBackground into the retryer and allow retries to continue while unfocused when it is true. Default behavior (pause while unfocused) is unchanged. Fixes #8353 --- .changeset/quiet-retries-keep-running.md | 5 +++ .../query-core/src/__tests__/query.test.tsx | 36 +++++++++++++++++++ packages/query-core/src/query.ts | 1 + packages/query-core/src/retryer.ts | 3 +- packages/query-core/src/types.ts | 10 +++--- 5 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 .changeset/quiet-retries-keep-running.md diff --git a/.changeset/quiet-retries-keep-running.md b/.changeset/quiet-retries-keep-running.md new file mode 100644 index 00000000000..78bcfaeaa7e --- /dev/null +++ b/.changeset/quiet-retries-keep-running.md @@ -0,0 +1,5 @@ +--- +'@tanstack/query-core': patch +--- + +Keep retrying failed queries while the tab is in the background when `refetchIntervalInBackground` is `true`. Previously the retryer paused until the page regained focus, which defeated the purpose of `refetchIntervalInBackground`. diff --git a/packages/query-core/src/__tests__/query.test.tsx b/packages/query-core/src/__tests__/query.test.tsx index ac4c10ea5b0..6acd17ecd55 100644 --- a/packages/query-core/src/__tests__/query.test.tsx +++ b/packages/query-core/src/__tests__/query.test.tsx @@ -105,6 +105,42 @@ describe('query', () => { expect(result).toBe('data3') }) + it('should keep retrying while unfocused when refetchIntervalInBackground is true', async () => { + const key = queryKey() + + // make page unfocused + const visibilityMock = mockVisibilityState('hidden') + + let count = 0 + let result + + const promise = queryClient.fetchQuery({ + queryKey: key, + queryFn: () => { + count++ + + if (count === 3) { + return `data${count}` + } + + throw new Error(`error${count}`) + }, + retry: 3, + retryDelay: 1, + refetchIntervalInBackground: true, + }) + + promise.then((data) => { + result = data + }) + + // Retries proceed even though the page stays unfocused + await vi.advanceTimersByTimeAsync(50) + expect(result).toBe('data3') + + visibilityMock.mockRestore() + }) + it('should continue retry after reconnect and resolve all promises', async () => { const key = queryKey() diff --git a/packages/query-core/src/query.ts b/packages/query-core/src/query.ts index 62bc9a16082..245da1dac04 100644 --- a/packages/query-core/src/query.ts +++ b/packages/query-core/src/query.ts @@ -559,6 +559,7 @@ export class Query< retry: context.options.retry, retryDelay: context.options.retryDelay, networkMode: context.options.networkMode, + refetchIntervalInBackground: context.options.refetchIntervalInBackground, canRun: () => true, }) diff --git a/packages/query-core/src/retryer.ts b/packages/query-core/src/retryer.ts index 0b1bb1833a1..da6a88ea177 100644 --- a/packages/query-core/src/retryer.ts +++ b/packages/query-core/src/retryer.ts @@ -18,6 +18,7 @@ interface RetryerConfig { retry?: RetryValue retryDelay?: RetryDelayValue networkMode: NetworkMode | undefined + refetchIntervalInBackground?: boolean canRun: () => boolean } @@ -102,7 +103,7 @@ export function createRetryer( } const canContinue = () => - focusManager.isFocused() && + (config.refetchIntervalInBackground === true || focusManager.isFocused()) && (config.networkMode === 'always' || onlineManager.isOnline()) && config.canRun() diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index b2e80f2a144..1551c4ee706 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -236,6 +236,11 @@ export interface QueryOptions< retry?: RetryValue retryDelay?: RetryDelayValue networkMode?: NetworkMode + /** + * If set to `true`, the query will continue to refetch (and retry) while their tab/window is in the background. + * Defaults to `false`. + */ + refetchIntervalInBackground?: boolean /** * The time in milliseconds that unused/inactive cache data remains in memory. * When a query's cache becomes unused or inactive, that cache data will be garbage collected after this duration. @@ -340,11 +345,6 @@ export interface QueryObserverOptions< | (( query: Query, ) => number | false | undefined) - /** - * If set to `true`, the query will continue to refetch while their tab/window is in the background. - * Defaults to `false`. - */ - refetchIntervalInBackground?: 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.