How do you poll an endpoint with TanStack Query v5's useQuery, and stop polling once a background job has finished?
answer
- an option, not a setInterval
- milliseconds or a function
- the function gets the query
- a falsy boolean ends it
- hidden tabs skip ticks
basics
~20 sSet refetchInterval on useQuery: a number of milliseconds polls on a fixed timer, and a function receiving the query can return false once the job is complete, which stops polling. Hidden tabs skip polling unless refetchIntervalInBackground is true.
solid answer
~40 sSet `refetchInterval` on `useQuery`. A number such as `2000` refetches every two seconds while the component is mounted, independently of `staleTime`. To stop when the work is done, pass a function: in v5 it receives the `Query` object, so read `query.state.data` and return `false` once the status is final, or the interval otherwise; if later data makes it return a number again, polling resumes. By default the timer skips fetches while the tab is hidden; set `refetchIntervalInBackground: true` when a dashboard must stay current in the background. Unmounting the component clears its timer, so there is no manual `setInterval` or cleanup to write.
code
tsx · 16 linesimport { useQuery } from '@tanstack/react-query'
type Job = { id: string; status: 'queued' | 'running' | 'complete' | 'failed' }
declare function fetchJob(id: string): Promise<Job>
export function useJobStatus(jobId: string) {
return useQuery({
queryKey: ['job', jobId],
queryFn: () => fetchJob(jobId),
refetchInterval: (query) => {
const status = query.state.data?.status
return status === 'complete' || status === 'failed' ? false : 2_000
},
})
}go deeper
Recall that polling is the refetchInterval option on useQuery, given in milliseconds, and that a function form can return false to stop.
Explain that the callback receives the query in v5, that polling ignores staleTime, and that hidden tabs skip ticks unless refetchIntervalInBackground is set.
Show you watch the request rate when several components poll one key, stop polling at terminal states, and pick polling only when push or focus refresh will not do.
Weigh polling load across many clients against push channels, and set team guidance on minimum intervals and background polling for costly endpoints.
## The option that replaces a hand-written timer Polling means asking the server again on a schedule, for example to follow a background job until it finishes. In TanStack Query v5 you do not write `setInterval` inside `useEffect` for this; you set **`refetchInterval`** on the query: - `refetchInterval: 5_000` refetches every five seconds. - `refetchInterval: false` is the default and means no polling. - `refetchInterval: (query) => number | false | undefined` computes the interval from the current state of the query. The timer belongs to the **observer**, the mounted `useQuery` call. It starts when the component mounts, and when the component unmounts the timer is cleared with it, so there is no cleanup code to write. Polling is **independent of `staleTime`**. Each tick refetches whether or not the data is fresh; `staleTime` only governs the event-driven triggers (a new mount, a window refocus, a reconnect). ## Stopping once the job is done Pass a function and read the latest result from the query it receives: ```tsx import { useQuery } from '@tanstack/react-query' type Job = { id: string; status: 'queued' | 'running' | 'complete' | 'failed' } declare function fetchJob(id: string): Promise<Job> export function useJobStatus(jobId: string) { return useQuery({ queryKey: ['job', jobId], queryFn: () => fetchJob(jobId), refetchInterval: (query) => { const status = query.state.data?.status return status === 'complete' || status === 'failed' ? false : 2_000 }, }) } ``` What happens, in order: 1. The component mounts and the first fetch runs. 2. After each update the function is called with the `Query` object. `query.state.data` is the raw result of the query function; a `select` transform does not apply there. 3. While the job is queued or running, the function returns `2_000` and the timer keeps firing. 4. When the status becomes `'complete'` or `'failed'`, it returns `false` and the timer is cleared. 5. If later data would make it return a number again, for example after the cache is updated for a restarted job, polling resumes automatically. The function can also close over component state. Returning `false` while a user has paused a live view, or while a required id is still missing, stops the timer, and returning a number again once the condition clears restarts it. This keeps every polling rule next to the query instead of spreading it across effects and refs. In v4 the callback received the data first and the query second. v5 passes **only the query**, which is why the snippet reads `query.state.data`. ## Hidden tabs and connectivity - **`refetchIntervalInBackground`** defaults to `false`. While the page is hidden, the timer still ticks but skips the fetch; polling picks up again once the tab is visible. Set it to `true` for a wall-mounted dashboard or anything that must stay current in a background tab. - In the default network mode, a fetch started while the browser reports being offline pauses until the connection returns rather than failing. ## Several components polling one key Each observer runs **its own** timer. Two mounted components reading `['job', id]` with `refetchInterval: 5_000` each fire every five seconds. What gets deduplicated is a fetch that overlaps one already in flight for that key, so if the two timers are not aligned, the server can see up to two requests per interval. Keep polling in one place, such as a single hook instance, or set the interval on only one of the readers. ## Choosing the right refresh mechanism | Need | Mechanism in TanStack Query v5 | |---|---| | Follow a long-running job until it ends | `refetchInterval` function returning `false` at the end | | Keep a dashboard current even in a background tab | `refetchInterval` plus `refetchIntervalInBackground: true` | | Refresh when the user comes back to the tab | `staleTime` with the default `refetchOnWindowFocus` | | Rarely changing data | a long `staleTime`, no polling at all | ## Common mistakes - Calling `refetch` from a `setInterval` in `useEffect`. It duplicates the option, needs manual cleanup and ignores tab visibility. - Expecting a long `staleTime` to slow polling down. The interval is the only thing that sets the polling rate. - Expecting polling to continue in a hidden tab without `refetchIntervalInBackground: true`. - Writing the v4 callback signature `(data, query)`, which in v5 receives the query where the data was expected.
- Does a long staleTime slow down polling in TanStack Query v5?No. `refetchInterval` runs on its own clock, and each tick refetches whether or not the data is fresh. `staleTime` only governs the event-driven triggers: a new mount, a window refocus and a reconnect. To poll less often, change the interval itself.
- Two mounted components read the same key with refetchInterval: 5000. How many requests reach the server?Each observer runs its own timer, so up to two per five seconds. Only a fetch that overlaps one already in flight for the key is deduplicated into a single request. Keep polling in one hook instance, or set the interval on only one of the readers.
saying these in an interview costs you the question
- You poll by calling refetch from a setInterval inside useEffect.
- A long staleTime makes refetchInterval skip ticks while the data is fresh.
- Polling keeps fetching in a hidden browser tab by default.
- In v5 the refetchInterval callback receives the data as its first argument.
- Polling keeps running after the component that set it unmounts.