Refetch on Window Focus and Reconnect
TanStack Query refetches stale data when the user returns to your tab or the network comes back — the mechanism behind the “always fresh when I look at it” feel of well-built apps. But the same mechanism, left unbounded, fires a burst of requests every time a user alt-tabs, which hammers your API and can produce visible loading flashes. This page is a concrete recipe within Background Refetch Strategies, and it explains exactly how refetchOnWindowFocus, refetchOnReconnect, the focusManager, and staleTime interact. For the polling-interval side of background revalidation, the companion page Optimizing SWR Revalidation Intervals tunes the time-based triggers rather than the event-based ones covered here.
The root cause of most focus-refetch complaints is a misunderstanding of the staleTime gate: focus and reconnect events do not force a refetch, they request one, and the request is only honored if the query is already stale.
Diagnostic Checklist
Before changing config, confirm which symptom you actually have:
- Every tab switch triggers a network request for a query you expected to stay cached —
staleTimeis0, so the query is stale the instant it settles. - A query never refetches on focus even though you set
refetchOnWindowFocus: true—staleTimeis long enough that the query is still fresh. - An expensive aggregate query reloads on every focus and causes a spinner flash — it needs a per-query opt-out, not a global one.
- Refetch on focus does nothing in React Native or an embedded webview — the default
focusManagerevents are not firing. - Requests fire on reconnect that you did not expect —
refetchOnReconnectdefaults totrueand is honored for any stale query.
Step-by-Step Implementation
Step 1 — Understand what triggers a focus and reconnect refetch
The focusManager and onlineManager are singletons that emit events. On the web, focusManager listens for visibilitychange and focus; onlineManager listens for the browser online/offline events. When either fires, TanStack Query walks active queries and evaluates whether each should refetch.
// query-client.ts
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
// Both default to true — every stale active query refetches on the event
refetchOnWindowFocus: true,
refetchOnReconnect: true,
// The gate: how long a query is considered fresh after it settles
staleTime: 30_000,
},
},
});
Cache Behavior Analysis. These options do not add event listeners per query — the managers are global and fire once per event. On each event TanStack Query iterates the active observers and, for each, checks the option flag and the staleness of the underlying query. A query with no mounted observer is skipped entirely, which is why a background tab’s unmounted queries do not stampede your API on focus.
Step 2 — Gate focus refetches with staleTime
staleTime is the single most important knob here. A focus event asks each active query to refetch, but TanStack Query only honors the request for queries that are stale. With staleTime: 0 (the default), a query is stale the instant it settles, so every focus refetches. Raising staleTime suppresses focus refetches within the fresh window.
import { useQuery } from '@tanstack/react-query';
function useUserProfile(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then((r) => r.json()),
// Fresh for 5 minutes — focus events inside this window do NOT refetch
staleTime: 5 * 60_000,
refetchOnWindowFocus: true, // still honored, but only once the query is stale
});
}
Cache Behavior Analysis. When the focus event fires, TanStack Query compares dataUpdatedAt + staleTime against the current time. If the data is still fresh, the focus request is treated as already satisfied by cache and no network call is made — the query stays in success state without transitioning to fetching. This is why refetchOnWindowFocus: true and a long staleTime coexist: the flag enables the trigger, staleTime decides whether the trigger does anything.
Step 3 — Configure per-query and disable for expensive queries
Global defaults are the baseline; override per query for expensive endpoints. Set refetchOnWindowFocus: false on a heavy aggregate so returning to the tab does not re-run it, while cheap queries keep the default behavior.
function useRevenueDashboard() {
return useQuery({
queryKey: ['revenue', 'dashboard'],
queryFn: fetchRevenueAggregate,
// Expensive multi-join query — do not re-run just because the tab regained focus
refetchOnWindowFocus: false,
// Still refetch when the network returns, since offline data may be very stale
refetchOnReconnect: true,
staleTime: 10 * 60_000,
});
}
Cache Behavior Analysis. Setting refetchOnWindowFocus: false removes this query from the focus-event walk entirely — the event fires but this observer opts out before the staleTime check. Keeping refetchOnReconnect: true means a genuine connectivity restoration still refetches, which matters because data fetched before going offline can be arbitrarily stale regardless of staleTime. Splitting the two flags lets you suppress the noisy trigger while keeping the meaningful one.
Step 4 — Customize the focusManager for non-browser environments
In React Native or an embedded webview, the browser visibilitychange event does not exist, so focus refetch silently never fires. Subscribe the focusManager to the platform’s own lifecycle so it reports focus correctly.
// focus-manager-native.ts
import { focusManager } from '@tanstack/react-query';
import { AppState, type AppStateStatus } from 'react-native';
focusManager.setEventListener((handleFocus) => {
const subscription = AppState.addEventListener('change', (status: AppStateStatus) => {
// Report focused whenever the app is in the foreground
handleFocus(status === 'active');
});
return () => subscription.remove();
});
Cache Behavior Analysis. focusManager.setEventListener replaces the default browser listener wholesale, so the callback you pass becomes the only source of focus signals. Calling handleFocus(true) runs the same active-query walk that a browser focus event would, honoring each query’s refetchOnWindowFocus and staleTime. Because the manager is a singleton, one registration at app startup covers every query in the tree.
- Treat
staleTimeas the real throttle for focus refetch;refetchOnWindowFocusonly enables or disables the trigger, it does not rate-limit it. - Disable
refetchOnWindowFocusper query for expensive endpoints instead of globally, so cheap queries keep their freshness benefit. - Keep
refetchOnReconnect: trueeven when you disable focus refetch — post-offline data can be stale beyond anystaleTime. - Register a custom
focusManagerlistener once at startup for React Native or webviews; without it focus refetch is a no-op.
Edge Cases and Gotchas
refetchOnWindowFocus: 'always'
Setting the option to the string 'always' bypasses the staleTime gate entirely — the query refetches on every focus regardless of freshness. This is occasionally correct for a live-trading ticker, but it is a common accidental cause of request storms when applied globally. Reserve 'always' for the handful of queries that truly must ignore staleTime.
Rapid focus/blur flapping
Switching windows quickly can fire multiple focus events in succession. TanStack Query deduplicates concurrent refetches for the same query via its request-in-flight tracking, so a second focus during an in-flight refetch does not launch a second request. You do not need your own debounce for this case; the query cache already collapses duplicate fetches.
Disabled queries and focus
A query with enabled: false is not an active fetching query, so focus and reconnect events never refetch it — even with refetchOnWindowFocus: true. If you gate a query behind enabled and expect focus to wake it, that will not happen; trigger it explicitly with refetch() or flip enabled instead.
Common Pitfalls and Resolutions
| Observable Issue | Root Cause | Diagnostic Resolution |
|---|---|---|
| Every tab switch fires a network request | staleTime is 0, so the query is stale immediately and every focus is honored |
Raise staleTime to the acceptable freshness window; the focus trigger stays enabled but is gated |
| Query never refetches on focus | staleTime is long enough that the query is still fresh when the event fires |
Lower staleTime, or set refetchOnWindowFocus: 'always' for queries that must ignore freshness |
| Expensive dashboard reloads on every focus with a spinner flash | Global refetchOnWindowFocus: true applies to the heavy query |
Set refetchOnWindowFocus: false on that specific query while keeping refetchOnReconnect: true |
| Focus refetch does nothing in React Native | The default focusManager listens for browser events that do not exist on native |
Register focusManager.setEventListener wired to AppState at app startup |
Frequently Asked Questions
Why does my query still not refetch on focus even with refetchOnWindowFocus true?
refetchOnWindowFocus only refetches queries that are stale. If staleTime has not elapsed since the data last settled, the query is fresh and TanStack Query treats the focus event as already satisfied by cache, so no request fires. Check the query’s staleTime — lower it, or set it to 0, if you want every focus to trigger a network refetch. The flag enables the trigger; staleTime decides whether the trigger reaches the network.
How do I stop an expensive dashboard query from refetching every time the tab regains focus?
Set refetchOnWindowFocus: false on that specific query so it opts out of the focus-event walk, or raise its staleTime so it stays fresh across typical focus intervals. Keeping the default refetchOnWindowFocus for cheap queries while disabling it per-query for the expensive one gives targeted control without a global override. Consider leaving refetchOnReconnect: true on the same query so a genuine reconnection still refreshes data that went stale while offline.
Does refetchOnWindowFocus work in React Native or does it need a custom focusManager?
The default focusManager listens for the browser visibilitychange and focus events, which do not exist in React Native, so focus refetch silently never fires there. Subscribe the focusManager to AppState via focusManager.setEventListener and call handleFocus(status === 'active') on change. After that one registration at startup, refetchOnWindowFocus and the staleTime gate behave exactly as they do on the web.
Related
- Background Refetch Strategies — the parent reference covering adaptive polling, visibility-driven revalidation, and network-aware fetchers across React Query, SWR, and RTK Query.
- Optimizing SWR Revalidation Intervals — the time-based counterpart to this event-based page, tuning
refreshInterval,dedupingInterval, and focus revalidation in SWR v2. - Cache Invalidation & Server Synchronization — the foundational pillar covering invalidation timing, refetch strategies, and the freshness guarantees that focus and reconnect refetch enforce.
- Mutation Sync & Rollback — how the
onSettledinvalidation after a mutation interacts with thestaleTimegate that governs focus refetch on this page.