Skip to content
Merged
3 changes: 2 additions & 1 deletion docs/src/pages/reference/useQuery.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,10 @@ const result = useQuery({
- `queryKeyHashFn: (queryKey: QueryKey) => string`
- Optional
- If specified, this function is used to hash the `queryKey` to a string.
- `refetchInterval: false | number`
- `refetchInterval: number | false | ((data: TData | undefined, query: Query) => number | false)`
- Optional
- If set to a number, all queries will continuously refetch at this frequency in milliseconds
- If set to a function, the function will be executed with the latest data and query to compute a frequency
- `refetchIntervalInBackground: boolean`
- Optional
- 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
Expand Down
24 changes: 18 additions & 6 deletions src/core/queryObserver.ts
Copy link
Contributor

Choose a reason for hiding this comment

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

I know a few years have passed and it's too late to say this, but using refetchInterval means using setInterval and it means it is short-polling not a long-polling.

The long-polling uses the Promise answer to set the next call, and it seems RQ hasn't received it yet.

cc: @TkDodo

Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export class QueryObserver<
private previousSelectError: Error | null
private staleTimeoutId?: number
private refetchIntervalId?: number
private currentRefetchInterval?: number | false
private trackedProps!: Array<keyof QueryObserverResult>

constructor(
Expand Down Expand Up @@ -187,14 +188,16 @@ export class QueryObserver<
this.updateStaleTimeout()
}

const nextRefetchInterval = this.computeRefetchInterval()

// Update refetch interval if needed
if (
mounted &&
(this.currentQuery !== prevQuery ||
this.options.enabled !== prevOptions.enabled ||
this.options.refetchInterval !== prevOptions.refetchInterval)
nextRefetchInterval !== this.currentRefetchInterval)
) {
this.updateRefetchInterval()
this.updateRefetchInterval(nextRefetchInterval)
}
}

Expand Down Expand Up @@ -365,13 +368,22 @@ export class QueryObserver<
}, timeout)
}

private updateRefetchInterval(): void {
private computeRefetchInterval() {
return typeof this.options.refetchInterval === 'function'
? this.options.refetchInterval(this.currentResult.data, this.currentQuery)
: this.options.refetchInterval ?? false
}

private updateRefetchInterval(nextInterval: number | false): void {
this.clearRefetchInterval()

this.currentRefetchInterval = nextInterval

if (
isServer ||
this.options.enabled === false ||
!isValidTimeout(this.options.refetchInterval)
!isValidTimeout(this.currentRefetchInterval) ||
this.currentRefetchInterval === 0
) {
return
}
Expand All @@ -383,12 +395,12 @@ export class QueryObserver<
) {
this.executeFetch()
}
}, this.options.refetchInterval)
}, this.currentRefetchInterval)
}

private updateTimers(): void {
this.updateStaleTimeout()
this.updateRefetchInterval()
this.updateRefetchInterval(this.computeRefetchInterval())
}

private clearTimers(): void {
Expand Down
11 changes: 9 additions & 2 deletions src/core/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { MutationState } from './mutation'
import type { QueryBehavior } from './query'
import type { QueryBehavior, Query } from './query'
import type { RetryValue, RetryDelayValue } from './retryer'
import type { QueryFilters } from './utils'

Expand Down Expand Up @@ -105,9 +105,16 @@ export interface QueryObserverOptions<
staleTime?: number
/**
* If set to a number, the query will continuously refetch at this frequency in milliseconds.
* If set to a function, the function will be executed with the latest data and query to compute a frequency
* Defaults to `false`.
*/
refetchInterval?: number | false
refetchInterval?:
| number
| false
| ((
data: TData | undefined,
query: Query<TQueryFnData, TError, TQueryData, TQueryKey>
) => number | false)
/**
* If set to `true`, the query will continue to refetch while their tab/window is in the background.
* Defaults to `false`.
Expand Down
2 changes: 1 addition & 1 deletion src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export function functionalUpdate<TInput, TOutput>(
: updater
}

export function isValidTimeout(value: any): value is number {
export function isValidTimeout(value: unknown): value is number {
return typeof value === 'number' && value >= 0 && value !== Infinity
}

Expand Down
91 changes: 91 additions & 0 deletions src/react/tests/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3520,6 +3520,97 @@ describe('useQuery', () => {
await waitFor(() => rendered.getByText('count: 2'))
})

it('should refetch in an interval depending on function result', async () => {
const key = queryKey()
let count = 0
const states: UseQueryResult<number>[] = []

function Page() {
const queryInfo = useQuery(key, () => count++, {
refetchInterval: (data = 0) => (data < 2 ? 10 : false),
})

states.push(queryInfo)

return <div>count: {queryInfo.data}</div>
}

const rendered = renderWithClient(queryClient, <Page />)

await waitFor(() => rendered.getByText('count: 2'))

expect(states.length).toEqual(6)

expect(states).toMatchObject([
{
status: 'loading',
isFetching: true,
data: undefined,
},
{
status: 'success',
isFetching: false,
data: 0,
},
{
status: 'success',
isFetching: true,
data: 0,
},
{
status: 'success',
isFetching: false,
data: 1,
},
{
status: 'success',
isFetching: true,
data: 1,
},
{
status: 'success',
isFetching: false,
data: 2,
},
])
})

it('should not interval fetch with a refetchInterval of 0', async () => {
const key = queryKey()
const states: UseQueryResult<number>[] = []

function Page() {
const queryInfo = useQuery(key, () => 1, {
refetchInterval: 0,
})

states.push(queryInfo)

return <div>count: {queryInfo.data}</div>
}

const rendered = renderWithClient(queryClient, <Page />)

await waitFor(() => rendered.getByText('count: 1'))

await sleep(10) //extra sleep to make sure we're not re-fetching

expect(states.length).toEqual(2)

expect(states).toMatchObject([
{
status: 'loading',
isFetching: true,
data: undefined,
},
{
status: 'success',
isFetching: false,
data: 1,
},
])
})

it('should accept an empty string as query key', async () => {
function Page() {
const result = useQuery('', ctx => ctx.queryKey)
Expand Down