Skip to content

Commit 9e414e8

Browse files
authored
feat(useQuery): make refetchInterval accept a function (#2622)
1 parent 36c02b6 commit 9e414e8

File tree

5 files changed

+121
-10
lines changed

5 files changed

+121
-10
lines changed

docs/src/pages/reference/useQuery.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,10 @@ const result = useQuery({
100100
- `queryKeyHashFn: (queryKey: QueryKey) => string`
101101
- Optional
102102
- If specified, this function is used to hash the `queryKey` to a string.
103-
- `refetchInterval: false | number`
103+
- `refetchInterval: number | false | ((data: TData | undefined, query: Query) => number | false)`
104104
- Optional
105105
- If set to a number, all queries will continuously refetch at this frequency in milliseconds
106+
- If set to a function, the function will be executed with the latest data and query to compute a frequency
106107
- `refetchIntervalInBackground: boolean`
107108
- Optional
108109
- If set to `true`, queries that are set to continuously refetch with a `refetchInterval` will continue to refetch while their tab/window is in the background

src/core/queryObserver.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export class QueryObserver<
7171
private previousSelectError: Error | null
7272
private staleTimeoutId?: number
7373
private refetchIntervalId?: number
74+
private currentRefetchInterval?: number | false
7475
private trackedProps!: Array<keyof QueryObserverResult>
7576

7677
constructor(
@@ -187,14 +188,16 @@ export class QueryObserver<
187188
this.updateStaleTimeout()
188189
}
189190

191+
const nextRefetchInterval = this.computeRefetchInterval()
192+
190193
// Update refetch interval if needed
191194
if (
192195
mounted &&
193196
(this.currentQuery !== prevQuery ||
194197
this.options.enabled !== prevOptions.enabled ||
195-
this.options.refetchInterval !== prevOptions.refetchInterval)
198+
nextRefetchInterval !== this.currentRefetchInterval)
196199
) {
197-
this.updateRefetchInterval()
200+
this.updateRefetchInterval(nextRefetchInterval)
198201
}
199202
}
200203

@@ -365,13 +368,22 @@ export class QueryObserver<
365368
}, timeout)
366369
}
367370

368-
private updateRefetchInterval(): void {
371+
private computeRefetchInterval() {
372+
return typeof this.options.refetchInterval === 'function'
373+
? this.options.refetchInterval(this.currentResult.data, this.currentQuery)
374+
: this.options.refetchInterval ?? false
375+
}
376+
377+
private updateRefetchInterval(nextInterval: number | false): void {
369378
this.clearRefetchInterval()
370379

380+
this.currentRefetchInterval = nextInterval
381+
371382
if (
372383
isServer ||
373384
this.options.enabled === false ||
374-
!isValidTimeout(this.options.refetchInterval)
385+
!isValidTimeout(this.currentRefetchInterval) ||
386+
this.currentRefetchInterval === 0
375387
) {
376388
return
377389
}
@@ -383,12 +395,12 @@ export class QueryObserver<
383395
) {
384396
this.executeFetch()
385397
}
386-
}, this.options.refetchInterval)
398+
}, this.currentRefetchInterval)
387399
}
388400

389401
private updateTimers(): void {
390402
this.updateStaleTimeout()
391-
this.updateRefetchInterval()
403+
this.updateRefetchInterval(this.computeRefetchInterval())
392404
}
393405

394406
private clearTimers(): void {

src/core/types.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { MutationState } from './mutation'
2-
import type { QueryBehavior } from './query'
2+
import type { QueryBehavior, Query } from './query'
33
import type { RetryValue, RetryDelayValue } from './retryer'
44
import type { QueryFilters } from './utils'
55

@@ -105,9 +105,16 @@ export interface QueryObserverOptions<
105105
staleTime?: number
106106
/**
107107
* If set to a number, the query will continuously refetch at this frequency in milliseconds.
108+
* If set to a function, the function will be executed with the latest data and query to compute a frequency
108109
* Defaults to `false`.
109110
*/
110-
refetchInterval?: number | false
111+
refetchInterval?:
112+
| number
113+
| false
114+
| ((
115+
data: TData | undefined,
116+
query: Query<TQueryFnData, TError, TQueryData, TQueryKey>
117+
) => number | false)
111118
/**
112119
* If set to `true`, the query will continue to refetch while their tab/window is in the background.
113120
* Defaults to `false`.

src/core/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export function functionalUpdate<TInput, TOutput>(
8787
: updater
8888
}
8989

90-
export function isValidTimeout(value: any): value is number {
90+
export function isValidTimeout(value: unknown): value is number {
9191
return typeof value === 'number' && value >= 0 && value !== Infinity
9292
}
9393

src/react/tests/useQuery.test.tsx

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3520,6 +3520,97 @@ describe('useQuery', () => {
35203520
await waitFor(() => rendered.getByText('count: 2'))
35213521
})
35223522

3523+
it('should refetch in an interval depending on function result', async () => {
3524+
const key = queryKey()
3525+
let count = 0
3526+
const states: UseQueryResult<number>[] = []
3527+
3528+
function Page() {
3529+
const queryInfo = useQuery(key, () => count++, {
3530+
refetchInterval: (data = 0) => (data < 2 ? 10 : false),
3531+
})
3532+
3533+
states.push(queryInfo)
3534+
3535+
return <div>count: {queryInfo.data}</div>
3536+
}
3537+
3538+
const rendered = renderWithClient(queryClient, <Page />)
3539+
3540+
await waitFor(() => rendered.getByText('count: 2'))
3541+
3542+
expect(states.length).toEqual(6)
3543+
3544+
expect(states).toMatchObject([
3545+
{
3546+
status: 'loading',
3547+
isFetching: true,
3548+
data: undefined,
3549+
},
3550+
{
3551+
status: 'success',
3552+
isFetching: false,
3553+
data: 0,
3554+
},
3555+
{
3556+
status: 'success',
3557+
isFetching: true,
3558+
data: 0,
3559+
},
3560+
{
3561+
status: 'success',
3562+
isFetching: false,
3563+
data: 1,
3564+
},
3565+
{
3566+
status: 'success',
3567+
isFetching: true,
3568+
data: 1,
3569+
},
3570+
{
3571+
status: 'success',
3572+
isFetching: false,
3573+
data: 2,
3574+
},
3575+
])
3576+
})
3577+
3578+
it('should not interval fetch with a refetchInterval of 0', async () => {
3579+
const key = queryKey()
3580+
const states: UseQueryResult<number>[] = []
3581+
3582+
function Page() {
3583+
const queryInfo = useQuery(key, () => 1, {
3584+
refetchInterval: 0,
3585+
})
3586+
3587+
states.push(queryInfo)
3588+
3589+
return <div>count: {queryInfo.data}</div>
3590+
}
3591+
3592+
const rendered = renderWithClient(queryClient, <Page />)
3593+
3594+
await waitFor(() => rendered.getByText('count: 1'))
3595+
3596+
await sleep(10) //extra sleep to make sure we're not re-fetching
3597+
3598+
expect(states.length).toEqual(2)
3599+
3600+
expect(states).toMatchObject([
3601+
{
3602+
status: 'loading',
3603+
isFetching: true,
3604+
data: undefined,
3605+
},
3606+
{
3607+
status: 'success',
3608+
isFetching: false,
3609+
data: 1,
3610+
},
3611+
])
3612+
})
3613+
35233614
it('should accept an empty string as query key', async () => {
35243615
function Page() {
35253616
const result = useQuery('', ctx => ctx.queryKey)

0 commit comments

Comments
 (0)