skip to content

Server-State Caching

TanStack Query in React Native needs its browser hooks rewired: focusManager to AppState, onlineManager to NetInfo, and a persister for the cache. Interviewers ask why refetch never fires.

part ofReact Nativeoverview, primer and where to startread it →
on this pageshow

explore

questions

5

With TanStack Query v5 in React Native, why does a stale portfolio query never refetch when the app returns to the foreground?

level: middleimportance: must knowfreq 55%

answer

  1. browser focus signal, not a phone one
  2. no document, no visibilitychange
  3. focusManager.setEventListener at app start
  4. AppState 'change' status === 'active'
  5. retries also pause while unfocused

basics

~20 s

TanStack Query's default focus signal is the browser's visibilitychange event, which React Native never fires, so the app always looks focused and no focus refetch happens. Wire focusManager to AppState, reporting focused only when the status is 'active'.

solid answer

~40 s

Out of the box `focusManager` listens for `visibilitychange` through `window.addEventListener`. React Native has a global `window` but no `addEventListener` on it and no `document`, so the default setup attaches nothing and `isFocused()` stays `true` forever. With no focus transition, `refetchOnWindowFocus` has nothing to react to, and the investing app shows the portfolio it fetched before the user left. The fix is one call at startup: `focusManager.setEventListener` with a setup that subscribes to `AppState.addEventListener('change', ...)`, passes `status === 'active'` to the callback, and returns `() => subscription.remove()`. Returning to `active` then flips focus back on, and every observed, enabled, stale query with `refetchOnWindowFocus` not set to `false` refetches. A useful side effect: failed requests stop retrying while the app is unfocused and resume on return.

code

typescript · 12 lines
typescript
// queryFocus.ts - import once from the app entry, before the QueryClientProvider renders
import { AppState, Platform } from 'react-native';
import { focusManager } from '@tanstack/react-query';

if (Platform.OS !== 'web') {
  focusManager.setEventListener((handleFocus) => {
    const subscription = AppState.addEventListener('change', (status) => {
      handleFocus(status === 'active');
    });
    return () => subscription.remove();
  });
}

go deeper

for a junior

Remember that TanStack Query's focus refetch listens for a browser event a phone never sends, and that AppState is React Native's foreground signal.

for a middle

Explain the default focusManager setup, why isFocused stays true without a document, and how setEventListener with an 'active' check restores the trigger.

for a senior

Show you know the side effects: iOS inactive transitions, retries pausing in the background, web guarded separately, and why staleTime, not the mapping, tames bursts.

for a principal

Frame focus, connectivity and screen focus as three separate refetch signals the team must wire deliberately, and decide which queries deserve each one.

## What TanStack Query means by focus TanStack Query refetches stale data at a few **trigger points**. One of them is **window focus**, switched per query by `refetchOnWindowFocus` (enabled by default). The signal comes from a singleton, `focusManager`, exported by `@tanstack/react-query`, and its default setup was written for a browser tab: - it registers a `visibilitychange` listener through `window.addEventListener`; - `isFocused()` reads `document.visibilityState` and treats anything other than `'hidden'` as focused; - when the focus state changes to focused, the `QueryClient` resumes paused mutations and then asks every cached query whether one of its observers wants a focus refetch. ## Why nothing fires on a phone React Native defines a global `window` (an alias of the global object) but gives it no `addEventListener`, and there is no `document` at all. The default setup checks for `window.addEventListener`, finds nothing and registers no listener. `isFocused()` then falls back to `document?.visibilityState !== 'hidden'`, which is `true` when there is no document. The consequences, in order: 1. the app is permanently **focused** as far as TanStack Query knows; 2. no transition ever happens, so the focus trigger never runs; 3. a portfolio that went stale while the investing app sat in the background is shown as it was when the user comes back, until a mount, an interval or a manual refresh fetches it again. Setting `refetchOnWindowFocus: 'always'` does not help. `'always'` only removes the staleness check; it still waits for a focus event that never arrives. ## The fix: feed focusManager from AppState React Native's foreground signal is the **`AppState`** module. Its `'change'` event delivers a status string: | Status | Platforms | Meaning | |---|---|---| | `active` | iOS, Android | the app is in the foreground | | `background` | iOS, Android | another app, the home screen, or (Android) another Activity | | `inactive` | iOS only | a transition, the multitasking view, Notification Center, an incoming call | Replace the default listener once, at app start, with **`focusManager.setEventListener`**. The setup function receives a callback: call it with `true` or `false`, and return a cleanup function. `AppState.addEventListener` returns a subscription whose `remove()` is exactly that cleanup, and `setEventListener` runs the previous cleanup whenever it is replaced. The mapping that matters is **focused only when the status is `'active'`**: - mapping `'background'` or `'inactive'` to focused makes queries refetch as the user *leaves*; - calling the callback with no argument re-evaluates `isFocused()`, which is still the browser fallback, so every change, including the move to background, is reported as focused; - the TanStack Query docs show an equivalent variant that calls `focusManager.setFocused(status === 'active')` from an `AppState` listener in a root `useEffect`; both write the same state. ## What changes once it is wired - Returning to `'active'` flips focus from `false` to `true`. Only **observed, enabled, stale** queries whose `refetchOnWindowFocus` is not `false` refetch; fresh ones stay as they are. - On iOS, pulling down Notification Center or taking a call produces `active`, then `inactive`, then `active` again, which also counts as a return to focus. That is normally harmless; if it produces bursts, give the portfolio query a longer `staleTime` rather than special-casing the status. - **Retries pause while unfocused.** After each retry delay the retryer checks `focusManager.isFocused()`; while the app is in the background it waits instead of retrying, and continues when focus returns. - On React Native Web the browser default already works, which is why the docs guard the handler with `Platform.OS !== 'web'`. ## Diagnosing it in a running app 1. Background the app, return, and log `focusManager.isFocused()` just before: `true` while backgrounded means the default listener is still in place. 2. If the wiring lives in a component effect rather than at module scope, confirm that component stays mounted; its cleanup removes the `AppState` subscription. 3. Confirm the query is actually stale on return; a fresh query correctly does nothing. ## What it does not cover `focusManager` tracks the **app** coming to the foreground. Moving between screens inside a running app is a different signal, owned by the navigation library, and does not flip it. Connectivity is a third signal, fed to TanStack Query through `onlineManager`, and needs its own wiring on React Native for the same reason: the browser events it listens to by default do not exist on a phone.

  • On iOS, pulling down Notification Center and releasing it refetches the portfolio. Why, and does it need fixing?
    iOS reports `inactive` while Notification Center, the multitasking view or a call covers the app, so mapping only `active` to focused turns that into an unfocused-then-focused transition, and stale queries refetch. Android has no `inactive` state, so it does not happen there. It is usually harmless; if the burst matters, lengthen that query's `staleTime` instead of changing the status mapping.
  • Why does the docs example skip the AppState handler when Platform.OS is 'web'?
    On React Native Web the code runs in a browser, where `window.addEventListener` and `document.visibilityState` exist and the default `visibilitychange` listener already tracks tab focus. The guard keeps that default on web and routes only iOS and Android through `AppState`.
  • What happens to a portfolio request that keeps failing while the user has the app in the background?
    With `focusManager` fed from `AppState`, the retryer checks focus after each retry delay. While the app is unfocused it pauses instead of retrying, so it stops hitting a failing server from the background, and it continues the remaining retries once `AppState` reports `active` again.

A shop bell wired to a door the building does not have: the shopkeeper never hears anyone come back in, so nothing is restocked until the bell is rewired to the door that exists, AppState's move to 'active'.

saying these in an interview costs you the question

  • refetchOnWindowFocus works on iOS and Android exactly as it does in a browser tab
  • Setting refetchOnWindowFocus to 'always' makes focus refetch fire in React Native
  • TanStack Query v5 subscribes to AppState by itself when it detects React Native
  • Treating 'background' as focused is fine because the next foreground event corrects it
  • Invalidating every query from an AppState listener is the intended wiring
open as a page

In a React Native app using TanStack Query v5, what must you add to get refetch on reconnect, and what changes while offline?

level: juniorimportance: should knowfreq 42%

basics

~20 s

Wire TanStack Query's onlineManager to NetInfo with onlineManager.setEventListener, calling setOnline from a NetInfo.addEventListener callback. Without it the library believes the phone is always online, so offline requests fail and retry instead of pausing and resuming on reconnect.

open as a page

With TanStack Query v5 wired to AppState and NetInfo in React Native, how many portfolio requests fire when focus and reconnect events arrive together?

level: middleimportance: should knowfreq 30%

basics

~20 s

One per query key. The focus and reconnect triggers both refetch with cancelRefetch set to false, so the second trigger finds a fetch already in flight and joins its promise instead of sending another request.

open as a page

A React Native investing app persists its TanStack Query v5 cache to AsyncStorage, yet the portfolio is empty after a cold start; what are the usual causes?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Usually one of four causes: gcTime shorter than maxAge dropped entries before the save, the store was discarded for age or a buster mismatch, the last refetch errored so the query was not saved, or queries ran before the async restore.

open as a page

In a React Native app using TanStack Query v5, when would you set networkMode to 'offlineFirst' or 'always' instead of the default 'online'?

level: seniorimportance: nice to knowfreq 20%

basics

~20 s

Keep 'online' for network queries. Use 'offlineFirst' when the queryFn might answer offline: it runs once and only pauses retries. Use 'always' when the queryFn never touches the network, such as an on-device storage read.

open as a page