Skip to content

Commit 2c351a4

Browse files
committed
fix(query-core): respect refetchIntervalInBackground option for query retries
1 parent a1b1279 commit 2c351a4

File tree

4 files changed

+141
-3
lines changed

4 files changed

+141
-3
lines changed

packages/query-core/src/__tests__/query.test.tsx

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,4 +1192,135 @@ describe('query', () => {
11921192
expect(initialDataFn).toHaveBeenCalledTimes(1)
11931193
expect(query.state.data).toBe('initial data')
11941194
})
1195+
1196+
1197+
it('should continue retry in background when refetchIntervalInBackground is true', async () => {
1198+
const key = queryKey()
1199+
1200+
// make page unfocused
1201+
const visibilityMock = mockVisibilityState('hidden')
1202+
1203+
let count = 0
1204+
let result
1205+
1206+
const promise = queryClient.fetchQuery({
1207+
queryKey: key,
1208+
queryFn: () => {
1209+
count++
1210+
1211+
if (count === 3) {
1212+
return `data${count}`
1213+
}
1214+
1215+
throw new Error(`error${count}`)
1216+
},
1217+
retry: 3,
1218+
retryDelay: 1,
1219+
refetchIntervalInBackground: true,
1220+
})
1221+
1222+
promise.then((data) => {
1223+
result = data
1224+
})
1225+
1226+
// Check if we do not have a result yet
1227+
expect(result).toBeUndefined()
1228+
1229+
// Query should continue retrying in background
1230+
await vi.advanceTimersByTimeAsync(50)
1231+
expect(result).toBe('data3')
1232+
1233+
// Reset visibilityState to original value
1234+
visibilityMock.mockRestore()
1235+
})
1236+
1237+
it('should pause retry when unfocused if refetchIntervalInBackground is false', async () => {
1238+
const key = queryKey()
1239+
1240+
// make page unfocused
1241+
const visibilityMock = mockVisibilityState('hidden')
1242+
1243+
let count = 0
1244+
let result
1245+
1246+
const promise = queryClient.fetchQuery({
1247+
queryKey: key,
1248+
queryFn: () => {
1249+
count++
1250+
1251+
if (count === 3) {
1252+
return `data${count}`
1253+
}
1254+
1255+
throw new Error(`error${count}`)
1256+
},
1257+
retry: 3,
1258+
retryDelay: 1,
1259+
refetchIntervalInBackground: false,
1260+
})
1261+
1262+
promise.then((data) => {
1263+
result = data
1264+
})
1265+
1266+
// Check if we do not have a result
1267+
expect(result).toBeUndefined()
1268+
1269+
// Check if the query is really paused
1270+
await vi.advanceTimersByTimeAsync(50)
1271+
expect(result).toBeUndefined()
1272+
1273+
// Reset visibilityState to original value
1274+
visibilityMock.mockRestore()
1275+
window.dispatchEvent(new Event('visibilitychange'))
1276+
1277+
// Query should now continue and resolve
1278+
await vi.advanceTimersByTimeAsync(50)
1279+
expect(result).toBe('data3')
1280+
})
1281+
1282+
it('should pause retry when unfocused if refetchIntervalInBackground is undefined (default behavior)', async () => {
1283+
const key = queryKey()
1284+
1285+
// make page unfocused
1286+
const visibilityMock = mockVisibilityState('hidden')
1287+
1288+
let count = 0
1289+
let result
1290+
1291+
const promise = queryClient.fetchQuery({
1292+
queryKey: key,
1293+
queryFn: () => {
1294+
count++
1295+
1296+
if (count === 3) {
1297+
return `data${count}`
1298+
}
1299+
1300+
throw new Error(`error${count}`)
1301+
},
1302+
retry: 3,
1303+
retryDelay: 1,
1304+
// refetchIntervalInBackground is not set (undefined by default)
1305+
})
1306+
1307+
promise.then((data) => {
1308+
result = data
1309+
})
1310+
1311+
// Check if we do not have a result
1312+
expect(result).toBeUndefined()
1313+
1314+
// Check if the query is really paused
1315+
await vi.advanceTimersByTimeAsync(50)
1316+
expect(result).toBeUndefined()
1317+
1318+
// Reset visibilityState to original value
1319+
visibilityMock.mockRestore()
1320+
window.dispatchEvent(new Event('visibilitychange'))
1321+
1322+
// Query should now continue and resolve
1323+
await vi.advanceTimersByTimeAsync(50)
1324+
expect(result).toBe('data3')
1325+
})
11951326
})

packages/query-core/src/query.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ export class Query<
378378
): Promise<TData> {
379379
if (
380380
this.state.fetchStatus !== 'idle' &&
381-
// If the promise in the retyer is already rejected, we have to definitely
381+
// If the promise in the retryer is already rejected, we have to definitely
382382
// re-start the fetch; there is a chance that the query is still in a
383383
// pending state when that happens
384384
this.#retryer?.status() !== 'rejected'
@@ -521,6 +521,7 @@ export class Query<
521521
retryDelay: context.options.retryDelay,
522522
networkMode: context.options.networkMode,
523523
canRun: () => true,
524+
refetchIntervalInBackground: this.options.refetchIntervalInBackground,
524525
})
525526

526527
try {

packages/query-core/src/retryer.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { focusManager } from './focusManager'
21
import { onlineManager } from './onlineManager'
32
import { pendingThenable } from './thenable'
43
import { isServer, sleep } from './utils'
4+
import { focusManager } from './focusManager'
55
import type { Thenable } from './thenable'
66
import type { CancelOptions, DefaultError, NetworkMode } from './types'
77

@@ -18,6 +18,7 @@ interface RetryerConfig<TData = unknown, TError = DefaultError> {
1818
retryDelay?: RetryDelayValue<TError>
1919
networkMode: NetworkMode | undefined
2020
canRun: () => boolean
21+
refetchIntervalInBackground?: boolean
2122
}
2223

2324
export interface Retryer<TData = unknown> {
@@ -100,7 +101,7 @@ export function createRetryer<TData = unknown, TError = DefaultError>(
100101
}
101102

102103
const canContinue = () =>
103-
focusManager.isFocused() &&
104+
(config.refetchIntervalInBackground === true || focusManager.isFocused()) &&
104105
(config.networkMode === 'always' || onlineManager.isOnline()) &&
105106
config.canRun()
106107

packages/query-core/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,11 @@ export interface QueryOptions<
275275
* Maximum number of pages to store in the data of an infinite query.
276276
*/
277277
maxPages?: number
278+
/**
279+
* If set to `true`, the query will continue to refetch while their tab/window is in the background.
280+
* Defaults to `false`.
281+
*/
282+
refetchIntervalInBackground?: boolean
278283
}
279284

280285
export interface InitialPageParam<TPageParam = unknown> {

0 commit comments

Comments
 (0)