From 200b63ac3d8aba3488ce45c1cf6780af079c7a2a Mon Sep 17 00:00:00 2001 From: Hellol Date: Wed, 20 Aug 2025 00:49:19 +0900 Subject: [PATCH] fix(query-core): respect refetchIntervalInBackground option for query retries --- .../query-core/src/__tests__/query.test.tsx | 130 ++++++++++++++++++ packages/query-core/src/query.ts | 3 +- packages/query-core/src/retryer.ts | 5 +- packages/query-core/src/types.ts | 5 + 4 files changed, 140 insertions(+), 3 deletions(-) diff --git a/packages/query-core/src/__tests__/query.test.tsx b/packages/query-core/src/__tests__/query.test.tsx index f11bf173d3..bf99986c52 100644 --- a/packages/query-core/src/__tests__/query.test.tsx +++ b/packages/query-core/src/__tests__/query.test.tsx @@ -1304,4 +1304,134 @@ describe('query', () => { data: 'data1', }) }) + + test('should continue retry in background 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 + }) + + // Check if we do not have a result yet + expect(result).toBeUndefined() + + // Query should continue retrying in background + await vi.advanceTimersByTimeAsync(50) + expect(result).toBe('data3') + + // Reset visibilityState to original value + visibilityMock.mockRestore() + }) + + test('should pause retry when unfocused if refetchIntervalInBackground is false', 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: false, + }) + + promise.then((data) => { + result = data + }) + + // Check if we do not have a result + expect(result).toBeUndefined() + + // Check if the query is really paused + await vi.advanceTimersByTimeAsync(50) + expect(result).toBeUndefined() + + // Reset visibilityState to original value + visibilityMock.mockRestore() + window.dispatchEvent(new Event('visibilitychange')) + + // Query should now continue and resolve + await vi.advanceTimersByTimeAsync(50) + expect(result).toBe('data3') + }) + + test('should pause retry when unfocused if refetchIntervalInBackground is undefined (default behavior)', 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 is not set (undefined by default) + }) + + promise.then((data) => { + result = data + }) + + // Check if we do not have a result + expect(result).toBeUndefined() + + // Check if the query is really paused + await vi.advanceTimersByTimeAsync(50) + expect(result).toBeUndefined() + + // Reset visibilityState to original value + visibilityMock.mockRestore() + window.dispatchEvent(new Event('visibilitychange')) + + // Query should now continue and resolve + await vi.advanceTimersByTimeAsync(50) + expect(result).toBe('data3') + }) }) diff --git a/packages/query-core/src/query.ts b/packages/query-core/src/query.ts index a34c8630dc..debb0b0f23 100644 --- a/packages/query-core/src/query.ts +++ b/packages/query-core/src/query.ts @@ -390,7 +390,7 @@ export class Query< ): Promise { if ( this.state.fetchStatus !== 'idle' && - // If the promise in the retyer is already rejected, we have to definitely + // If the promise in the retryer is already rejected, we have to definitely // re-start the fetch; there is a chance that the query is still in a // pending state when that happens this.#retryer?.status() !== 'rejected' @@ -541,6 +541,7 @@ export class Query< retryDelay: context.options.retryDelay, networkMode: context.options.networkMode, canRun: () => true, + refetchIntervalInBackground: this.options.refetchIntervalInBackground, }) try { diff --git a/packages/query-core/src/retryer.ts b/packages/query-core/src/retryer.ts index f4ada851c9..572b1f207f 100644 --- a/packages/query-core/src/retryer.ts +++ b/packages/query-core/src/retryer.ts @@ -1,7 +1,7 @@ -import { focusManager } from './focusManager' import { onlineManager } from './onlineManager' import { pendingThenable } from './thenable' import { isServer, sleep } from './utils' +import { focusManager } from './focusManager' import type { Thenable } from './thenable' import type { CancelOptions, DefaultError, NetworkMode } from './types' @@ -18,6 +18,7 @@ interface RetryerConfig { retryDelay?: RetryDelayValue networkMode: NetworkMode | undefined canRun: () => boolean + refetchIntervalInBackground?: boolean } export interface Retryer { @@ -101,7 +102,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 df6ea8c173..7d9ad9af31 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -275,6 +275,11 @@ export interface QueryOptions< * Maximum number of pages to store in the data of an infinite query. */ maxPages?: number + /** + * If set to `true`, the query will continue to refetch while their tab/window is in the background. + * Defaults to `false`. + */ + refetchIntervalInBackground?: boolean } export interface InitialPageParam {