From 66debba622999024ab1e17ebbb47a2ad268ce26c Mon Sep 17 00:00:00 2001 From: Zelys Date: Tue, 24 Mar 2026 12:13:23 -0500 Subject: [PATCH 1/3] docs(react): add polling guide Adds a dedicated guide for refetchInterval-based polling. The option was only mentioned in passing in important-defaults.md with no explanation of how it works, how to adapt it dynamically, or how it interacts with window focus, networkMode, and the enabled flag. Covers: - Basic setup and independence from staleTime - Dynamic intervals via function form - refetchIntervalInBackground for dashboards / always-on UIs - Disabling window-focus refetching for fullscreen game and kiosk UIs - Pausing polling with the enabled flag - networkMode: 'always' for unreliable navigator.onLine environments - Deduplication behavior across multiple observers Updates config.json to add the guide to the React sidebar between Window Focus Refetching and Disabling/Pausing Queries. Adds a cross-reference in important-defaults.md. --- docs/config.json | 4 + .../react/guides/important-defaults.md | 2 +- docs/framework/react/guides/polling.md | 119 ++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 docs/framework/react/guides/polling.md diff --git a/docs/config.json b/docs/config.json index 54eeb77e838..1ea67a58add 100644 --- a/docs/config.json +++ b/docs/config.json @@ -246,6 +246,10 @@ "label": "Window Focus Refetching", "to": "framework/react/guides/window-focus-refetching" }, + { + "label": "Polling", + "to": "framework/react/guides/polling" + }, { "label": "Disabling/Pausing Queries", "to": "framework/react/guides/disabling-queries" diff --git a/docs/framework/react/guides/important-defaults.md b/docs/framework/react/guides/important-defaults.md index f17e777e566..2f35fd08b8d 100644 --- a/docs/framework/react/guides/important-defaults.md +++ b/docs/framework/react/guides/important-defaults.md @@ -21,7 +21,7 @@ Out of the box, TanStack Query is configured with **aggressive but sane** defaul > Setting `staleTime` is the recommended way to avoid excessive refetches, but you can also customize the points in time for refetches by setting options like `refetchOnMount`, `refetchOnWindowFocus` and `refetchOnReconnect`. -- Queries can optionally be configured with a `refetchInterval` to trigger refetches periodically, which is independent of the `staleTime` setting. +- Queries can optionally be configured with a `refetchInterval` to trigger refetches periodically, which is independent of the `staleTime` setting. See [Polling](./polling.md) for details. - Query results that have no more active instances of `useQuery`, `useInfiniteQuery` or query observers are labeled as "inactive" and remain in the cache in case they are used again at a later time. - By default, "inactive" queries are garbage collected after **5 minutes**. diff --git a/docs/framework/react/guides/polling.md b/docs/framework/react/guides/polling.md new file mode 100644 index 00000000000..072fb5be61c --- /dev/null +++ b/docs/framework/react/guides/polling.md @@ -0,0 +1,119 @@ +--- +id: polling +title: Polling +--- + +`refetchInterval` makes a query refetch on a timer. Set it to a number in milliseconds and the query runs every N ms while there's at least one active observer: + +```tsx +useQuery({ + queryKey: ['prices'], + queryFn: fetchPrices, + refetchInterval: 5_000, // every 5 seconds +}) +``` + +Polling is independent of `staleTime`. A query can be fresh and still poll on schedule — `staleTime` controls when background refetches triggered by *mounting* or *window focus* happen. `refetchInterval` fires on its own clock regardless. + +## Adapting the interval to query state + +Pass a function instead of a number to compute the interval from the current query. The function receives the `Query` object and should return a number in ms or `false` to stop polling: + +```tsx +useQuery({ + queryKey: ['job', jobId], + queryFn: () => fetchJobStatus(jobId), + refetchInterval: (query) => { + // Stop polling once the job finishes + if (query.state.data?.status === 'complete') return false + return 2_000 + }, +}) +``` + +Returning `false` clears the interval timer. If the query result changes so the function would return a positive number again, polling resumes automatically. + +## Background polling + +By default, polling pauses when the browser tab loses focus. For dashboards or any interface where data needs to stay current even while the user is in another tab, disable that behavior: + +```tsx +useQuery({ + queryKey: ['portfolio'], + queryFn: fetchPortfolio, + refetchInterval: 30_000, + refetchIntervalInBackground: true, +}) +``` + +[//]: # 'Example1' + +## Disabling window-focus refetching in non-browser UIs + +In a fullscreen game, kiosk app, or any UI where the window is always technically "active," focus events don't map to user intent. Relying on them for freshness typically causes a burst of requests whenever the user alt-tabs. + +Disable focus-based refetching globally and use `refetchInterval` instead: + +```tsx +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + refetchOnWindowFocus: false, + refetchInterval: 60_000, + }, + }, +}) +``` + +If you need to tie polling to your own notion of "active" (for example, a game session), wire up `focusManager.setEventListener` with your own signal: + +```tsx +import { focusManager } from '@tanstack/react-query' + +focusManager.setEventListener((handleFocus) => { + const onActive = () => handleFocus(true) + const onIdle = () => handleFocus(false) + + gameSession.on('active', onActive) + gameSession.on('idle', onIdle) + + return () => { + gameSession.off('active', onActive) + gameSession.off('idle', onIdle) + } +}) +``` + +See [Window Focus Refetching](./window-focus-refetching.md) for the full `focusManager` API. + +## Pausing polling + +Set `enabled: false` to stop polling when conditions aren't met. Any running interval is cleared immediately, and it restarts when `enabled` becomes `true` again: + +```tsx +useQuery({ + queryKey: ['prices', tokenAddress], + queryFn: () => fetchPrice(tokenAddress), + refetchInterval: 15_000, + enabled: !!tokenAddress && !isPaused, +}) +``` + +## Polling with offline support + +By default, queries skip fetches when the browser reports no network connection. If your app runs in environments where `navigator.onLine` is unreliable — embedded browsers, Electron, some WebViews — set `networkMode: 'always'` to ignore the online check: + +```tsx +useQuery({ + queryKey: ['chainStatus'], + queryFn: fetchChainStatus, + refetchInterval: 10_000, + networkMode: 'always', +}) +``` + +For more on network modes, see [Network Mode](./network-mode.md). + +## Note on deduplication + +Multiple components mounting the same query key with `refetchInterval` do not stack their timers. The cache has one interval per query, so two components both using `refetchInterval: 5000` on the same key produce one request every 5 seconds, not two. From 32df0e7aba3d6890fd3903a3c545e2fbe6d10ff9 Mon Sep 17 00:00:00 2001 From: Zelys Date: Tue, 24 Mar 2026 12:19:58 -0500 Subject: [PATCH 2/3] docs(react/polling): add example markers for framework portability --- docs/framework/react/guides/polling.md | 28 +++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/docs/framework/react/guides/polling.md b/docs/framework/react/guides/polling.md index 072fb5be61c..421a70ae856 100644 --- a/docs/framework/react/guides/polling.md +++ b/docs/framework/react/guides/polling.md @@ -5,6 +5,8 @@ title: Polling `refetchInterval` makes a query refetch on a timer. Set it to a number in milliseconds and the query runs every N ms while there's at least one active observer: +[//]: # 'Example1' + ```tsx useQuery({ queryKey: ['prices'], @@ -13,12 +15,16 @@ useQuery({ }) ``` +[//]: # 'Example1' + Polling is independent of `staleTime`. A query can be fresh and still poll on schedule — `staleTime` controls when background refetches triggered by *mounting* or *window focus* happen. `refetchInterval` fires on its own clock regardless. ## Adapting the interval to query state Pass a function instead of a number to compute the interval from the current query. The function receives the `Query` object and should return a number in ms or `false` to stop polling: +[//]: # 'Example2' + ```tsx useQuery({ queryKey: ['job', jobId], @@ -31,12 +37,16 @@ useQuery({ }) ``` +[//]: # 'Example2' + Returning `false` clears the interval timer. If the query result changes so the function would return a positive number again, polling resumes automatically. ## Background polling By default, polling pauses when the browser tab loses focus. For dashboards or any interface where data needs to stay current even while the user is in another tab, disable that behavior: +[//]: # 'Example3' + ```tsx useQuery({ queryKey: ['portfolio'], @@ -46,7 +56,7 @@ useQuery({ }) ``` -[//]: # 'Example1' +[//]: # 'Example3' ## Disabling window-focus refetching in non-browser UIs @@ -54,6 +64,8 @@ In a fullscreen game, kiosk app, or any UI where the window is always technicall Disable focus-based refetching globally and use `refetchInterval` instead: +[//]: # 'Example4' + ```tsx const queryClient = new QueryClient({ defaultOptions: { @@ -65,8 +77,12 @@ const queryClient = new QueryClient({ }) ``` +[//]: # 'Example4' + If you need to tie polling to your own notion of "active" (for example, a game session), wire up `focusManager.setEventListener` with your own signal: +[//]: # 'Example5' + ```tsx import { focusManager } from '@tanstack/react-query' @@ -84,12 +100,16 @@ focusManager.setEventListener((handleFocus) => { }) ``` +[//]: # 'Example5' + See [Window Focus Refetching](./window-focus-refetching.md) for the full `focusManager` API. ## Pausing polling Set `enabled: false` to stop polling when conditions aren't met. Any running interval is cleared immediately, and it restarts when `enabled` becomes `true` again: +[//]: # 'Example6' + ```tsx useQuery({ queryKey: ['prices', tokenAddress], @@ -99,10 +119,14 @@ useQuery({ }) ``` +[//]: # 'Example6' + ## Polling with offline support By default, queries skip fetches when the browser reports no network connection. If your app runs in environments where `navigator.onLine` is unreliable — embedded browsers, Electron, some WebViews — set `networkMode: 'always'` to ignore the online check: +[//]: # 'Example7' + ```tsx useQuery({ queryKey: ['chainStatus'], @@ -112,6 +136,8 @@ useQuery({ }) ``` +[//]: # 'Example7' + For more on network modes, see [Network Mode](./network-mode.md). ## Note on deduplication From dd9b863d938783bc7851438742a933a76e0e2a78 Mon Sep 17 00:00:00 2001 From: Zelys Date: Tue, 24 Mar 2026 12:40:12 -0500 Subject: [PATCH 3/3] =?UTF-8?q?docs(react):=20fix=20deduplication=20note?= =?UTF-8?q?=20=E2=80=94=20timers=20are=20per=20observer,=20not=20per=20que?= =?UTF-8?q?ry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/framework/react/guides/polling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/framework/react/guides/polling.md b/docs/framework/react/guides/polling.md index 421a70ae856..148d40a0846 100644 --- a/docs/framework/react/guides/polling.md +++ b/docs/framework/react/guides/polling.md @@ -142,4 +142,4 @@ For more on network modes, see [Network Mode](./network-mode.md). ## Note on deduplication -Multiple components mounting the same query key with `refetchInterval` do not stack their timers. The cache has one interval per query, so two components both using `refetchInterval: 5000` on the same key produce one request every 5 seconds, not two. +Each `QueryObserver` — each component using `useQuery` with `refetchInterval` — runs its own timer. Two components subscribed to the same key with `refetchInterval: 5000` each fire their timer every 5 seconds. What's deduplicated is concurrent in-flight fetches: if two timers fire at overlapping moments, React Query won't issue two parallel network requests for the same key. The second fetch is held until the first settles. In practice, two components on the same polling interval produce one request per cycle, but the timers are observer-level, not query-level.