| | |

TanStack Query 06 ⚛️ Window Focus — Managing auto-refetch behavior

⏱️ Estimated read: 11 min | |

Window focus refetching is one of TanStack Query’s most visible defaults. When a user switches to another tab, leaves the browser, or minimizes the window, the queries become inactive. When the user returns, the queries are observed again, and if their data is stale, TanStack Query requests fresh data in the background. The component does not show a loading spinner; it continues to display the cached data while the request runs. The behavior is designed to keep the UI current without interrupting the user, but it is also the source of the most common “why is my app fetching?” question during development, because focusing the browser’s DevTools window also triggers a refetch .

This chapter covers the refetchOnWindowFocus option, the global and per-query configuration points, the focusManager API for custom focus events, and the interaction between window focus and staleTime. It builds on the stale and cache configuration from the previous chapter: a query that is fresh will not refetch on focus, regardless of the refetchOnWindowFocus setting.

Key point: refetchOnWindowFocus defaults to true, but the refetch only occurs if the data is stale. While the data is within its staleTime window, returning to the tab does not trigger a network request. To disable focus refetching entirely, set the option to false globally or per query. To make focus refetch even when data is fresh, set it to 'always'.


Why window focus refetching exists

The stale-tab problem.

A user opens a dashboard, switches to email, and returns ten minutes later. The dashboard’s data is ten minutes old. Without focus refetching, the user sees stale numbers until the component remounts or the data is manually refreshed. Focus refetching solves this by detecting the return to the tab and triggering a background request if the data has exceeded its staleTime. The user sees the old data immediately, and the new data replaces it when the request resolves .

The development-noise problem.

The default behavior is aggressive enough to surprise developers during development. Switching between the code editor, the browser, and the DevTools panel counts as window focus changes, and each return triggers a refetch for any stale query. The TanStack Query documentation explicitly warns that refetches during development are more frequent because focusing between DevTools and the application also causes a fetch . This is not a bug; it is the default behavior working as designed.

The tab-visibility problem.

TanStack Query’s default focus handler listens to the visibilitychange event rather than the focus event. The handler checks document.visibilityState === 'visible' to determine whether the tab is active . This is more reliable than the focus event because it correctly handles cases where the window is focused but the tab is not visible, such as when the browser window is visible but a different tab is selected. The DevTools “Emulate a focused page” setting can interfere with this detection by keeping visibilityState set to visible even when the tab is inactive .


a. Disabling focus refetching globally and per query

The refetchOnWindowFocus option accepts true, false, 'always', or a function that receives the query and returns one of those values. It can be set globally in defaultOptions or per query in the useQuery call.

// Global: disable for all queries
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,   // No automatic focus refetch
    },
  },
})

// Per query: disable for one query
function TodoList() {
  const { data } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
    refetchOnWindowFocus: false,     // Overrides global default
  })
  // ...
}

The global setting in defaultOptions applies to every query that does not override it. The per-query setting overrides the global value for that specific query . A query that is fresh (staleTime has not elapsed) will not refetch on focus even if refetchOnWindowFocus is true, because the refetch is gated by staleness. Setting refetchOnWindowFocus: 'always' bypasses the staleness check and forces a refetch on every focus, but this is rarely the correct choice for production applications.


b. Customizing focus events with focusManager

The focusManager provides control over what counts as a focus event. The default handler listens to visibilitychange, but a custom handler can listen to other events or integrate with a different focus detection mechanism.

import { focusManager } from '@tanstack/react-query'

// Replace the default focus handler
focusManager.setEventListener((handleFocus) => {
  // Custom event: window focus instead of visibilitychange
  const onFocus = () => handleFocus(true)
  const onBlur = () => handleFocus(false)

  window.addEventListener('focus', onFocus, false)
  window.addEventListener('blur', onBlur, false)

  return () => {
    window.removeEventListener('focus', onFocus)
    window.removeEventListener('blur', onBlur)
  }
})

The setEventListener callback receives a handleFocus function. The custom handler calls handleFocus(true) when the window is focused and handleFocus(false) when it loses focus. The return value is a cleanup function that removes the listeners, which TanStack Query calls when a new handler is set . The default handler is replaced entirely; the visibilitychange listener is removed.

The focusManager.setFocused method can also be used to manually control the focus state. Calling focusManager.setFocused(true) treats the window as focused, and focusManager.setFocused(undefined) reverts to the default detection . This is useful in testing environments or in non-browser contexts where the focus state is controlled by other means.


c. React Native and the AppState integration

React Native does not have a window object or a visibilitychange event. The default focus handler does not function in a React Native environment. The AppState module provides the equivalent information: the app is either active, background, or inactive.

import { AppState, Platform } from 'react-native'
import { focusManager } from '@tanstack/react-query'

function onAppStateChange(status: AppStateStatus) {
  if (Platform.OS !== 'web') {
    focusManager.setFocused(status === 'active')
  }
}

useEffect(() => {
  const subscription = AppState.addEventListener('change', onAppStateChange)
  return () => subscription.remove()
}, [])

The AppState listener calls focusManager.setFocused with true when the app becomes active and false when it moves to the background . This restores the focus-refetch behavior on mobile, where the app regaining focus is the equivalent of a browser tab becoming visible.


Complete Example Session

// ============================================
// PART 1: THE GLOBAL DEFAULT
// ============================================
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

// Disable focus refetching globally for this application
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5,       // Data is fresh for 5 minutes
      refetchOnWindowFocus: false,    // No focus refetch, even when stale
    },
  },
})

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Dashboard />
    </QueryClientProvider>
  )
}

// ============================================
// PART 2: PER-QUERY OVERRIDE
// ============================================
import { useQuery } from '@tanstack/react-query'

function Dashboard() {
  // This query refetches on focus despite the global false
  const { data: alerts } = useQuery({
    queryKey: ['alerts'],
    queryFn: fetchAlerts,
    staleTime: 30_000,              // Fresh for 30 seconds
    refetchOnWindowFocus: true,     // Override: refetch when stale
  })

  // This query uses the global default (no focus refetch)
  const { data: stats } = useQuery({
    queryKey: ['stats'],
    queryFn: fetchStats,
  })

  return (
    <div>
      <AlertList alerts={alerts} />
      <StatsPanel stats={stats} />
    </div>
  )
}

// ============================================
// PART 3: THE ALWAYS OPTION
// ============================================
// Force refetch on every focus, even if data is fresh
const { data: livePrice } = useQuery({
  queryKey: ['price', symbol],
  queryFn: () => fetchPrice(symbol),
  staleTime: 10_000,                // Fresh for 10 seconds
  refetchOnWindowFocus: 'always',   // Ignores staleness on focus
})

// ============================================
// PART 4: CUSTOM FOCUS EVENT
// ============================================
import { focusManager } from '@tanstack/react-query'

// Use window focus/blur instead of visibilitychange
focusManager.setEventListener((handleFocus) => {
  const onFocus = () => handleFocus(true)
  const onBlur = () => handleFocus(false)

  window.addEventListener('focus', onFocus, false)
  window.addEventListener('blur', onBlur, false)

  return () => {
    window.removeEventListener('focus', onFocus)
    window.removeEventListener('blur', onBlur)
  }
})

// ============================================
// PART 5: MANUAL FOCUS CONTROL
// ============================================
// Treat the app as focused (useful in tests or special contexts)
focusManager.setFocused(true)

// Revert to the default focus detection
focusManager.setFocused(undefined)

// ============================================
// PART 6: REACT NATIVE APSTATE
// ============================================
// In a React Native app, replace the default handler with AppState
import { useEffect } from 'react'
import { AppState, Platform } from 'react-native'
import type { AppStateStatus } from 'react-native'

function useAppStateFocus() {
  useEffect(() => {
    const subscription = AppState.addEventListener('change', (status: AppStateStatus) => {
      if (Platform.OS !== 'web') {
        focusManager.setFocused(status === 'active')
      }
    })
    return () => subscription.remove()
  }, [])
}

The six parts demonstrate the full configuration surface: the global default, the per-query override, the 'always' option, the custom focus event, the manual focus control, and the React Native integration. The critical distinction is between Parts 1 and 2: the global setting disables focus refetching for most queries, but a specific query can override it when freshness matters more than network quietness.


Quick Reference

refetchOnWindowFocus Values

ValueBehavior
true (default)Refetch on focus if data is stale
falseNever refetch on focus
'always'Refetch on every focus, even if fresh
(query) => valueDynamic decision per query

Configuration Points

ScopeLocationApplies To
GlobalQueryClient defaultOptionsAll queries
Per-queryuseQuery optionsThat query

focusManager Methods

MethodPurpose
setEventListener(fn)Replace the default focus handler
setFocused(true)Manually mark as focused
setFocused(false)Manually mark as unfocused
setFocused(undefined)Revert to default detection

Default Handler

EventDetection
visibilitychangedocument.visibilityState === 'visible'

Best Practices

✅ Do This:

// Set a staleTime that matches data volatility
staleTime: 1000 * 60 * 5                                     # ✅

// Disable globally for data that should not refetch
refetchOnWindowFocus: false                                   # ✅

// Override per query when freshness matters
refetchOnWindowFocus: true                                    # ✅

// Use 'always' only for real-time data
refetchOnWindowFocus: 'always'                                # ✅

// Replace the handler in React Native
focusManager.setFocused(status === 'active')                  # ✅

// Check the DevTools "Emulate a focused page" setting when testing
// It prevents focus refetch from firing                          # ✅

❌ Don’t Do This:

// Expect focus refetch when data is fresh
// staleTime gates the refetch regardless of the option           # ❌

// Use 'always' for every query
// It defeats the purpose of caching and staleTime                # ❌

// Assume the default handler works in React Native
// The window object does not exist                               # ❌

// Forget to clean up custom event listeners
// The handler must return a cleanup function                     # ❌

// Rely on focus refetch for real-time updates
// Use refetchInterval or WebSockets for that                     # ❌

Common Pitfalls

PitfallWhy It HappensFix
Refetch fires during developmentDevTools focus counts as window focusDisable in dev or set refetchOnWindowFocus: false
No refetch on tab returnData is still fresh (within staleTime)Lower staleTime or set 'always'
Focus refetch does not workDevTools “Emulate a focused page” enabledDisable the setting in the Rendering panel
React Native focus brokenDefault handler relies on windowUse AppState with focusManager.setFocused
Custom handler not removedMissing cleanup returnReturn the removal function
Unexpected refetches after setFocused(true)Manual focus state overrides detectionReset with setFocused(undefined)

Real-World Examples

1. Disable globally

new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false } } })

2. Enable per query

useQuery({ queryKey: ['alerts'], queryFn: fetchAlerts, refetchOnWindowFocus: true })

3. Always refetch on focus

useQuery({ queryKey: ['live'], queryFn: fetchLive, refetchOnWindowFocus: 'always' })

4. Custom focus event

focusManager.setEventListener((handleFocus) => {
  window.addEventListener('focus', () => handleFocus(true))
  window.addEventListener('blur', () => handleFocus(false))
  return () => { /* cleanup */ }
})

5. Manual focus state

focusManager.setFocused(true)

6. Revert focus state

focusManager.setFocused(undefined)

7. React Native AppState

focusManager.setFocused(status === 'active')

8. Check staleTime interaction

staleTime: 60_000  // No focus refetch for 1 minute

9. Disable for a specific query with global true

useQuery({ queryKey: ['static'], queryFn: fetchStatic, refetchOnWindowFocus: false })

10. Verify with DevTools

// DevTools → Rendering → disable "Emulate a focused page"

Visual

Window Focus Refetch Flow

┌──────────────────────────────────────────────────────────────┐
│  FOCUS REFETCH IS GATED BY STALENESS                         │
│                                                              │
│  User leaves tab → query is inactive                         │
│       │                                                      │
│       ▼                                                      │
│  User returns → visibilitychange fires                       │
│       │                                                      │
│       ▼                                                      │
│  focusManager detects focus                                  │
│       │                                                      │
│       ▼                                                      │
│  Query is observed again                                     │
│       │                                                      │
│       ├── Data is FRESH (within staleTime)                   │
│       │     └── No refetch; cached data shown                │
│       │                                                      │
│       └── Data is STALE (past staleTime)                     │
│             └── Background refetch runs                      │
│                   └── Cached data shown until new data arrives│
└──────────────────────────────────────────────────────────────┘

Global vs Per-Query

┌──────────────────────────────────────────────────────────────┐
│  THE PER-QUERY OPTION OVERRIDES THE GLOBAL DEFAULT           │
│                                                              │
│  Global: refetchOnWindowFocus: false                         │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ Query A: (no override) → false (global)                │  │
│  │ Query B: refetchOnWindowFocus: true → true (override)  │  │
│  │ Query C: refetchOnWindowFocus: 'always' → always       │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  The global setting is the default; the per-query setting    │
│  is the exception.                                           │
└──────────────────────────────────────────────────────────────┘

focusManager Custom Handler

┌──────────────────────────────────────────────────────────────┐
│  REPLACING THE DEFAULT FOCUS HANDLER                         │
│                                                              │
│  Default handler:                                            │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ visibilitychange → document.visibilityState === 'visible'│ │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  Custom handler:                                             │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ focusManager.setEventListener((handleFocus) => {       │  │
│  │   window.addEventListener('focus', () => handleFocus(true))│
│  │   window.addEventListener('blur', () => handleFocus(false))│
│  │   return () => { /* remove listeners */ }              │  │
│  │ })                                                     │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  The default handler is removed when a custom one is set.    │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Default behaviorRefetch on focus if data is stale
Default valuetrue
Disable globallydefaultOptions.queries.refetchOnWindowFocus: false
Disable per queryrefetchOnWindowFocus: false in useQuery
Force alwaysrefetchOnWindowFocus: 'always'
Staleness gateFresh data never refetches on focus
Default eventvisibilitychange with document.visibilityState
Custom handlerfocusManager.setEventListener
Manual focusfocusManager.setFocused(true/false/undefined)
React NativeAppState + focusManager.setFocused

Key takeaways:

  • Focus refetch is gated by staleTime. Data that is still fresh does not trigger a refetch when the window regains focus, even if refetchOnWindowFocus is true. The option controls whether the refetch is allowed; the staleness check controls whether it happens .
  • The default is true, but it only fires for stale data. Setting it to false disables the behavior entirely. Setting it to 'always' bypasses the staleness check and should be reserved for real-time data.
  • The default handler listens to visibilitychange, not focus. This is more reliable across browsers and handles cases where the window is focused but the tab is not visible .
  • DevTools can interfere with focus detection. The “Emulate a focused page” setting keeps document.visibilityState set to visible, preventing the visibilitychange event from firing. Disable it when testing focus refetch behavior .
  • React Native requires a custom integration. The window object does not exist in React Native. The AppState module provides the focus information, and focusManager.setFocused applies it to TanStack Query .
  • Custom handlers must return a cleanup function. focusManager.setEventListener removes the previous handler and uses the new one. The cleanup function ensures that the old event listeners are removed when the handler is replaced .

Remember: Window focus refetching is the mechanism that keeps stale data current when the user returns to the tab. It is controlled by refetchOnWindowFocus, gated by staleTime, and implemented by the focusManager‘s visibilitychange listener. Disable it globally for applications where background fetches are unwanted, enable it per query for data that needs to stay current, and use 'always' only for real-time dashboards. In React Native, replace the default handler with the AppState integration. The behavior is not magic; it is a configurable policy that balances freshness against network activity.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!