Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions packages/query-core/src/__tests__/query.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})
})
3 changes: 2 additions & 1 deletion packages/query-core/src/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ export class Query<
): Promise<TData> {
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'
Expand Down Expand Up @@ -541,6 +541,7 @@ export class Query<
retryDelay: context.options.retryDelay,
networkMode: context.options.networkMode,
canRun: () => true,
refetchIntervalInBackground: this.options.refetchIntervalInBackground,
})

try {
Expand Down
5 changes: 3 additions & 2 deletions packages/query-core/src/retryer.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -18,6 +18,7 @@ interface RetryerConfig<TData = unknown, TError = DefaultError> {
retryDelay?: RetryDelayValue<TError>
networkMode: NetworkMode | undefined
canRun: () => boolean
refetchIntervalInBackground?: boolean
}

export interface Retryer<TData = unknown> {
Expand Down Expand Up @@ -101,7 +102,7 @@ export function createRetryer<TData = unknown, TError = DefaultError>(
}

const canContinue = () =>
focusManager.isFocused() &&
(config.refetchIntervalInBackground === true || focusManager.isFocused()) &&
(config.networkMode === 'always' || onlineManager.isOnline()) &&
config.canRun()

Expand Down
5 changes: 5 additions & 0 deletions packages/query-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm we shouldn’t really add this to QueryOptions as it’s an observer level property.

Now that I’m seeing this, I don’t think it’s “fixable” with an additional check because you can have different observers with different values for refetchIntervalInBackground.

I think the best step forward is to just remove the check in v6 and document the current behaviour for v5.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand your point about the issue.

I'll close this pr and submit docs explaining the current v5 behavior with some alternative approaches.

Thank you for the feedback!

}

export interface InitialPageParam<TPageParam = unknown> {
Expand Down