From 66debba622999024ab1e17ebbb47a2ad268ce26c Mon Sep 17 00:00:00 2001 From: Zelys Date: Tue, 24 Mar 2026 12:13:23 -0500 Subject: [PATCH 1/7] 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/7] 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/7] =?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. From 0fc4ddfdfc550ec03ac88b271569856ea8790fd8 Mon Sep 17 00:00:00 2001 From: Zelys Date: Mon, 30 Mar 2026 11:30:10 -0500 Subject: [PATCH 4/7] docs(react): address review feedback on polling guide - Remove staleTime enumeration; link to Important Defaults instead - Remove game/kiosk focus management examples (scope creep) - Rewrite pausing polling to use refetchInterval function instead of enabled: false - Fix offline support section: connectivity uses online/offline events, not navigator.onLine - Fix deduplication note: remove queuing implication and em dash - Add non-browser environments note pointing to React Native guide --- docs/framework/react/guides/polling.md | 72 ++++++-------------------- 1 file changed, 16 insertions(+), 56 deletions(-) diff --git a/docs/framework/react/guides/polling.md b/docs/framework/react/guides/polling.md index 148d40a0846..f99171a4bc2 100644 --- a/docs/framework/react/guides/polling.md +++ b/docs/framework/react/guides/polling.md @@ -17,7 +17,7 @@ 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. +Polling is independent of `staleTime`. A query can be fresh and still poll on schedule; see [Important Defaults](./important-defaults.md) for how `staleTime` interacts with other refetch behaviors. `refetchInterval` fires on its own clock regardless of freshness. ## Adapting the interval to query state @@ -58,74 +58,30 @@ useQuery({ [//]: # 'Example3' -## 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: - -[//]: # 'Example4' - -```tsx -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - refetchOnWindowFocus: false, - refetchInterval: 60_000, - }, - }, -}) -``` - -[//]: # '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' - -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) - } -}) -``` - -[//]: # '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: +Pass a function to `refetchInterval` and close over component state to control when polling runs: -[//]: # 'Example6' +[//]: # 'Example4' ```tsx useQuery({ queryKey: ['prices', tokenAddress], queryFn: () => fetchPrice(tokenAddress), - refetchInterval: 15_000, - enabled: !!tokenAddress && !isPaused, + refetchInterval: () => { + if (!tokenAddress || isPaused) return false + return 15_000 + }, }) ``` -[//]: # 'Example6' +[//]: # 'Example4' ## 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: +TanStack Query detects connectivity by listening to the browser's `online` and `offline` events. In environments where those events don't fire reliably (Electron, some embedded WebViews), set `networkMode: 'always'` to skip the connectivity check: -[//]: # 'Example7' +[//]: # 'Example5' ```tsx useQuery({ @@ -136,10 +92,14 @@ useQuery({ }) ``` -[//]: # 'Example7' +[//]: # 'Example5' For more on network modes, see [Network Mode](./network-mode.md). ## Note on deduplication -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. +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 gets deduplicated is concurrent in-flight fetches: if two timers fire at the same time, only one network request goes out. The timers are observer-level; the deduplication is query-level. + +## Non-browser environments + +For non-browser runtimes like React Native, the standard `online`/`offline` and focus events aren't available. The [React Native guide](../react-native.md) covers how to connect `focusManager` and `onlineManager` to native app state APIs. From a0a1023daf857f1f794171507a0b03940c632a2b Mon Sep 17 00:00:00 2001 From: Zelys Date: Tue, 31 Mar 2026 10:11:45 -0500 Subject: [PATCH 5/7] docs(solid,vue): add polling guide framework ports --- docs/config.json | 8 +++ docs/framework/solid/guides/polling.md | 71 ++++++++++++++++++++++++++ docs/framework/vue/guides/polling.md | 6 +++ 3 files changed, 85 insertions(+) create mode 100644 docs/framework/solid/guides/polling.md create mode 100644 docs/framework/vue/guides/polling.md diff --git a/docs/config.json b/docs/config.json index 1ea67a58add..b781bb35b34 100644 --- a/docs/config.json +++ b/docs/config.json @@ -403,6 +403,10 @@ "label": "Window Focus Refetching", "to": "framework/solid/guides/window-focus-refetching" }, + { + "label": "Polling", + "to": "framework/solid/guides/polling" + }, { "label": "Disabling/Pausing Queries", "to": "framework/solid/guides/disabling-queries" @@ -540,6 +544,10 @@ "label": "Window Focus Refetching", "to": "framework/vue/guides/window-focus-refetching" }, + { + "label": "Polling", + "to": "framework/vue/guides/polling" + }, { "label": "Disabling/Pausing Queries", "to": "framework/vue/guides/disabling-queries" diff --git a/docs/framework/solid/guides/polling.md b/docs/framework/solid/guides/polling.md new file mode 100644 index 00000000000..0fa6cbe3384 --- /dev/null +++ b/docs/framework/solid/guides/polling.md @@ -0,0 +1,71 @@ +--- +id: polling +title: Polling +ref: docs/framework/react/guides/polling.md +replace: { '@tanstack/react-query': '@tanstack/solid-query' } +--- + +[//]: # 'Example1' + +```tsx +useQuery(() => ({ + queryKey: ['prices'], + queryFn: fetchPrices, + refetchInterval: 5_000, // every 5 seconds +})) +``` + +[//]: # 'Example1' +[//]: # 'Example2' + +```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 + }, +})) +``` + +[//]: # 'Example2' +[//]: # 'Example3' + +```tsx +useQuery(() => ({ + queryKey: ['portfolio'], + queryFn: fetchPortfolio, + refetchInterval: 30_000, + refetchIntervalInBackground: true, +})) +``` + +[//]: # 'Example3' +[//]: # 'Example4' + +```tsx +useQuery(() => ({ + queryKey: ['prices', tokenAddress], + queryFn: () => fetchPrice(tokenAddress), + refetchInterval: () => { + if (!tokenAddress || isPaused) return false + return 15_000 + }, +})) +``` + +[//]: # 'Example4' +[//]: # 'Example5' + +```tsx +useQuery(() => ({ + queryKey: ['chainStatus'], + queryFn: fetchChainStatus, + refetchInterval: 10_000, + networkMode: 'always', +})) +``` + +[//]: # 'Example5' diff --git a/docs/framework/vue/guides/polling.md b/docs/framework/vue/guides/polling.md new file mode 100644 index 00000000000..74f6b99067e --- /dev/null +++ b/docs/framework/vue/guides/polling.md @@ -0,0 +1,6 @@ +--- +id: polling +title: Polling +ref: docs/framework/react/guides/polling.md +replace: { '@tanstack/react-query': '@tanstack/vue-query' } +--- From 3d22c4581667558d686be6cccd52e2008f238124 Mon Sep 17 00:00:00 2001 From: Zelys Date: Tue, 31 Mar 2026 10:23:51 -0500 Subject: [PATCH 6/7] docs: wrap React Native section in markers for framework port exclusion --- docs/framework/react/guides/polling.md | 4 ++++ docs/framework/solid/guides/polling.md | 2 ++ docs/framework/vue/guides/polling.md | 2 ++ 3 files changed, 8 insertions(+) diff --git a/docs/framework/react/guides/polling.md b/docs/framework/react/guides/polling.md index f99171a4bc2..487a42f227e 100644 --- a/docs/framework/react/guides/polling.md +++ b/docs/framework/react/guides/polling.md @@ -100,6 +100,10 @@ For more on network modes, see [Network Mode](./network-mode.md). 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 gets deduplicated is concurrent in-flight fetches: if two timers fire at the same time, only one network request goes out. The timers are observer-level; the deduplication is query-level. +[//]: # 'ReactNative' + ## Non-browser environments For non-browser runtimes like React Native, the standard `online`/`offline` and focus events aren't available. The [React Native guide](../react-native.md) covers how to connect `focusManager` and `onlineManager` to native app state APIs. + +[//]: # 'ReactNative' diff --git a/docs/framework/solid/guides/polling.md b/docs/framework/solid/guides/polling.md index 0fa6cbe3384..9410627ebfc 100644 --- a/docs/framework/solid/guides/polling.md +++ b/docs/framework/solid/guides/polling.md @@ -69,3 +69,5 @@ useQuery(() => ({ ``` [//]: # 'Example5' +[//]: # 'ReactNative' +[//]: # 'ReactNative' diff --git a/docs/framework/vue/guides/polling.md b/docs/framework/vue/guides/polling.md index 74f6b99067e..d5e1c80ee45 100644 --- a/docs/framework/vue/guides/polling.md +++ b/docs/framework/vue/guides/polling.md @@ -4,3 +4,5 @@ title: Polling ref: docs/framework/react/guides/polling.md replace: { '@tanstack/react-query': '@tanstack/vue-query' } --- +[//]: # 'ReactNative' +[//]: # 'ReactNative' From 6cff7d1a9c4b5ceebb7f88d9e96c415ddc5787da Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:25:05 +0000 Subject: [PATCH 7/7] ci: apply automated fixes --- docs/framework/vue/guides/polling.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/framework/vue/guides/polling.md b/docs/framework/vue/guides/polling.md index d5e1c80ee45..1f096cd007d 100644 --- a/docs/framework/vue/guides/polling.md +++ b/docs/framework/vue/guides/polling.md @@ -4,5 +4,6 @@ title: Polling ref: docs/framework/react/guides/polling.md replace: { '@tanstack/react-query': '@tanstack/vue-query' } --- + [//]: # 'ReactNative' [//]: # 'ReactNative'