skip to content

A React app using TanStack Query v5 sends a burst of requests each time a user switches back to its browser tab; why, and how would you fix it without hiding real updates?

level: seniorimportance: should knowfreq 62%

answer

  1. what counts as stale by default
  2. which event fires on tab return
  3. the focus refetch flag's default
  4. tune freshness, not the trigger
  5. one reader's setting is enough

basics

~10 s

Default staleTime 0 makes all cached data stale and refetchOnWindowFocus defaults to true, so returning to the tab refetches every active query. Set realistic staleTime values rather than disabling focus refetching globally.

solid answer

~50 s

In v5, TanStack Query's `focusManager` listens to `visibilitychange`, so returning to the tab counts as a refocus. With the default refetch flags, every query with a mounted observer whose data is stale refetches on that event in the background, and with the default `staleTime: 0` all data is stale, so a dashboard with twenty active keys fires twenty requests. Inactive queries are skipped, and each key fetches once however many components read it. The fix is to give each kind of data a real `staleTime`, as an app-wide default on the `QueryClient` with per-query overrides, so recently fetched data stays fresh and the refocus skips it. Making every reader of a key agree matters, because one observer with the default re-enables the refetch. Setting `refetchOnWindowFocus: false` everywhere also stops the burst, but users then come back to data nobody rechecked.

code

tsx · 14 lines
tsx
import { QueryClient, useQuery } from '@tanstack/react-query'

declare function fetchCountries(): Promise<string[]>
declare function fetchNotifications(): Promise<unknown[]>

export const queryClient = new QueryClient({
  defaultOptions: { queries: { staleTime: 30_000 } },
})

export const useCountries = () =>
  useQuery({ queryKey: ['countries'], queryFn: fetchCountries, staleTime: 'static' })

export const useNotifications = () =>
  useQuery({ queryKey: ['notifications'], queryFn: fetchNotifications, staleTime: 5_000 })

go deeper

for a junior

Recall that TanStack Query refetches stale queries when the user returns to the tab, and that the default staleTime of 0 makes every query stale.

for a middle

Explain the three conditions for a focus refetch: a mounted observer, a refetchOnWindowFocus value that allows it, and stale data, with one request per key.

for a senior

Diagnose which keys refetch, find readers of one key with mismatched staleTime, and fix freshness per kind of data instead of switching the trigger off everywhere.

for a principal

Frame staleness budgets per data category as a team convention, weighing server load from refocus bursts against users acting on outdated numbers.

## What TanStack Query treats as a window focus TanStack Query does not watch components for this; a singleton called the **`focusManager`** watches the page. In v5 its default listener subscribes to the window's **`visibilitychange`** event and reports the page as focused when `document.visibilityState` is `'visible'`. v4 also listened to the `focus` event; v5 dropped it. The practical result: - switching back to the browser tab, or usually restoring a minimised window, counts as a refocus; - clicking back into the page from browser DevTools or closing a native dialog does not. When the manager reports focus, the query cache walks **every query it holds**. For each one it looks for the first observer that wants a focus refetch and, if it finds one, refetches that query **once**, without cancelling a fetch already in flight. ## Why the refocus becomes a burst A query refetches on refocus only when all of these hold: 1. It has at least one **mounted observer**, meaning some component currently uses it. Inactive entries waiting for garbage collection are skipped. 2. That observer's **`refetchOnWindowFocus`** is `true` (the default), `'always'`, or a function returning one of those. 3. For `true`, the data is **stale for that observer**: its `staleTime` has elapsed since the data was written, or the query was invalidated. The default `staleTime` is `0`, so condition 3 is always met. A dashboard with twenty active query keys therefore sends twenty requests every time the user returns to the tab, however recently each was fetched. Ten components reading the same key still produce one request, because the refetch is per query, not per component. One nuance catches people who already set a `staleTime`: **staleness is judged per observer**. If one component reads `['user']` with `staleTime: 5 * 60 * 1000` and another reads the same key with no `staleTime`, the second observer sees stale data and triggers the refetch for the whole key. One hook that forgot the setting defeats it for every reader. ## Diagnosing it - Watch the network panel or the TanStack Query Devtools while switching tabs, and list the keys that refetch. - For each key, find the effective `staleTime`: the `QueryClient` default merged with each `useQuery` call's own options. Check that every reader of the key agrees. - Check that the refetch is actually harmful. A focus refetch is a background fetch; the cached data should stay on screen while it runs. If the screen flashes a full loading state, the component is rendering its loader from the wrong flag, which is a separate bug. - If refetches seem not to fire at all while testing with devtools open, check for a devtools setting that **emulates a focused page**: it keeps the page visible and suppresses `visibilitychange`. ## Fixing it without hiding real updates | Approach | Effect | Cost | |---|---|---| | A realistic app-wide `staleTime`, such as 30-60 seconds | refocus soon after a fetch skips the query | one number must suit most data | | Per-query `staleTime` by kind of data | a live feed stays short, a profile long, reference data `Infinity` or `'static'` | needs shared hooks or query options so readers agree | | `refetchOnWindowFocus: false` on one query | that query never refetches on refocus | its data can be arbitrarily old when the user returns | | `refetchOnWindowFocus: false` app-wide | no bursts at all | every screen can show outdated numbers after a long absence | | a function `(query) => …` for `refetchOnWindowFocus` | decide per query at runtime | more logic to maintain | The first two rows are the real fix. They keep the trigger and change what counts as stale, so a user returning after a minute sees no requests, while one returning after an hour gets fresh data automatically. Switching the trigger off globally removes the symptom and the freshness together. ## Related knobs worth knowing - `refetchOnWindowFocus: 'always'` refetches on every refocus even when data is fresh, unless `staleTime` is `'static'`. Reserve it for cheap requests whose data must be rechecked on return. - `refetchOnReconnect` works the same way for regained network connectivity, so the same `staleTime` fix also tames reconnect bursts. - Outside the browser, for example in React Native, you wire focus yourself with `focusManager.setEventListener` or `focusManager.setFocused`.

  • Two components read ['user'], one with a five-minute staleTime and one with none. Does the key refetch on refocus?
    Yes. On refocus the query refetches if any of its observers allows focus refetching and sees stale data. The observer with the default `staleTime` of 0 sees stale data, so the key refetches once regardless of the other component's setting. Put the `staleTime` in a shared hook or query options object so every reader agrees.
  • Why might focus refetching appear not to work while you test it with browser devtools open?
    v5 relies only on `visibilitychange`. A devtools setting that emulates a focused page keeps `document.visibilityState` visible and suppresses those events, so switching tabs never registers as a refocus. Turn that emulation off in the devtools rendering settings, then switch tabs to test.
  • When is refetchOnWindowFocus: 'always' justified?
    When data must be rechecked on every return however recently it was fetched, such as a prominent balance or unread count, and the request is cheap. It bypasses `staleTime` for the focus trigger only, and it is still blocked when `staleTime` is `'static'`.

saying these in an interview costs you the question

  • The burst happens because React remounts every component when the tab regains focus.
  • Setting refetchOnWindowFocus: false app-wide is the standard fix and has no downside.
  • Raising gcTime keeps the data fresh across tab switches.
  • Every component using the same key sends its own request on refocus.
  • Inactive cached queries also refetch when the window regains focus.
  • TanStack Query v5 refetches when the window focus event fires, such as clicking back into the page.