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.