With TanStack Query in a React Native app, how do paused mutations work offline, and what must you wire up so they resume after reconnecting or restarting?
answer
- networkMode 'online' pauses, retry defaults to 0
- onMutate runs before the pause
- no browser online events in React Native
- onlineManager.setEventListener with NetInfo
- setMutationDefaults so restored mutations find mutationFn
basics
~20 sWith networkMode 'online', a TanStack mutation made offline pauses and resumes when onlineManager reports online. React Native has no browser online events, so wire onlineManager to NetInfo; for restarts, persist mutations and register mutationFn with setMutationDefaults.
solid answer
~40 sA TanStack Query v5 mutation defaults to `networkMode: 'online'`: if the `onlineManager` says offline, it enters `isPaused: true` instead of failing, after `onMutate` has already applied any optimistic update. When the mounted `QueryClient` sees the manager go online, it calls `resumePausedMutations()`. In React Native the manager's default setup finds no browser `online` events and stays online forever, so without `onlineManager.setEventListener` wired to `NetInfo.addEventListener` mutations never pause and, with the default `retry: 0`, fail straight to `onError`. Paused mutations also live only in memory: to survive a restart you persist the cache, register the function with `setMutationDefaults(mutationKey, { mutationFn })` because functions are not serialized, and call `resumePausedMutations()` after the restore. A shared `scope.id` makes replay serial.
code
typescript · 19 linesimport NetInfo from '@react-native-community/netinfo';
import { onlineManager, QueryClient } from '@tanstack/react-query';
import { postScan, type Scan } from './api';
// React Native has no browser online/offline events: feed NetInfo in.
onlineManager.setEventListener(setOnline =>
NetInfo.addEventListener(state => {
setOnline(!!state.isConnected);
}),
);
export const queryClient = new QueryClient();
// A default mutationFn lets paused mutations restored from storage resume.
queryClient.setMutationDefaults(['recordScan'], {
mutationFn: (scan: Scan) => postScan(scan),
scope: { id: 'scans' }, // replay scans one at a time, in order
retry: 3,
});go deeper
Remember that a TanStack mutation made offline can pause and resume later, but only if onlineManager knows about connectivity, which React Native must wire up with NetInfo.
Explain networkMode 'online', the paused state, onMutate running first, automatic resume through the mounted QueryClient, and why functions must come from setMutationDefaults after a restore.
Show the production setup: onlineManager wired to NetInfo, persisted paused mutations with default mutationFns, resume after restore, scoped serial replay and a retry policy for transient errors.
Decide whether TanStack's paused mutations are the offline queue or a dedicated durable outbox is, based on how long writes must survive and who else must replay them.
## What a paused mutation is In TanStack Query v5, a **mutation** is a write run through `useMutation` or `queryClient` defaults. Its `networkMode` defaults to `'online'`: before the first attempt, the mutation asks the `onlineManager` whether the app is online. If it is not, the mutation does not fail; it enters the **paused** state (`isPaused: true` while `status` is `'pending'`) and waits. Mutations do not retry by default (`retry` is `0`), so pausing is what keeps an offline write alive. Two details matter for offline-first apps: - **`onMutate` runs before the pause.** An optimistic update written in `onMutate` shows immediately, even though the request has not been sent. - **Paused mutations resume on their own.** When the `QueryClient` is mounted (by `QueryClientProvider`), it subscribes to the `onlineManager`; when that reports online, it calls `resumePausedMutations()`, which continues every paused mutation. It also resumes them when the focus manager reports the app focused. Calling `queryClient.resumePausedMutations()` while offline does nothing. ## The React Native trap: the online manager never goes offline On the web, the `onlineManager` listens to the browser's `online` and `offline` events. In React Native those events do not exist: the manager's default setup checks for `window.addEventListener`, finds none, and registers nothing. The manager starts in the **online** state, so in an unconfigured React Native app it **never** reports offline. The effect is that mutations never pause: a write made in a dead zone is attempted, fails with a network error, and, with the default `retry: 0`, goes straight to `onError`. The TanStack Query React Native guide fixes this by wiring the manager to NetInfo: ```tsx onlineManager.setEventListener(setOnline => NetInfo.addEventListener(state => { setOnline(!!state.isConnected); }), ); ``` `NetInfo.addEventListener` returns an unsubscribe function, which is exactly the cleanup `setEventListener` expects. The guide also shows an `expo-network` variant for Expo apps. ## Surviving an app restart Pausing only helps while the process lives. The mutation cache is in memory, so a killed app loses its paused mutations unless the cache is persisted and restored (the query-cache persister setup is its own subject). Persistence has a catch the TanStack docs spell out: 1. Only the mutation's **state** is persisted, because functions cannot be serialized. 2. After a restore, the component that created the mutation may not be mounted, so `resumePausedMutations()` can fail with `No mutationFn found`. 3. The fix is to register a default with `queryClient.setMutationDefaults(mutationKey, { mutationFn })` and give each `useMutation` the same `mutationKey`, so a restored mutation finds its function by key. 4. After the restore succeeds (the docs use `PersistQueryClientProvider`'s `onSuccess`), call `queryClient.resumePausedMutations()`. ## Keeping replay in order By default, mutations run **in parallel**, even several calls of the same one. A reconnect that resumes twenty paused scans at once sends twenty requests at once, in no guaranteed order. Give related mutations a **scope**: all mutations with the same `scope.id` run serially, queued in the paused state until the one ahead of them finishes. | Setting | Default | Offline-first choice | |---|---|---| | `networkMode` | `'online'` | Keep it, so writes pause offline | | `retry` (mutations) | `0` | A small number or a function for transient errors | | `onlineManager` in React Native | always online | Wire to NetInfo or `expo-network` | | `scope` | none, parallel | `{ id: 'scans' }` for ordered replay | | `mutationFn` location | per `useMutation` | `setMutationDefaults` when mutations are persisted | ## Observing the queue in the UI `useMutation` returns `isPaused` alongside `status`, so a scan button can show "waiting for signal" rather than a spinner that never ends. For a global indicator, `useMutationState` can read every mutation with a given key and count the paused ones, which gives pickers the pending count they need to trust the app. Because `onMutate` has already applied the optimistic change, the list itself looks complete; the pending markers are what tell the user it is not yet on the server. ## When a hand-written outbox is still the better fit TanStack's paused mutations are a convenient outbox for data the app already fetches through TanStack Query. A dedicated outbox in durable storage is simpler to reason about when writes must survive for days, be inspected by support, or be replayed by background work outside React. Either way, the replay rules are the same: in order, one at a time per resource, and removed only after the server confirms.
- With TanStack Query in React Native, why does a scan fired in a dead zone hit onError instead of pausing when onlineManager is not configured?The `onlineManager` starts online and, in React Native, its default setup registers no listener because there are no browser `online`/`offline` events. It therefore never reports offline, the mutation is attempted, the request fails with a network error, and mutations default to `retry: 0`, so the error is final. Wiring the manager to NetInfo makes the mutation pause instead.
- In TanStack Query, what does giving mutations the same scope.id change during replay?Mutations normally run in parallel, even repeated calls of the same one. Mutations sharing a `scope.id` run serially: each new one starts paused while another in that scope is in progress and resumes when its turn comes. For an offline queue this keeps replay in the order the user worked and avoids a burst of concurrent requests on reconnect.
saying these in an interview costs you the question
- TanStack Query detects offline automatically in React Native, just as in a browser.
- A paused mutation has already failed and must be fired again by hand.
- Paused mutations survive an app restart without any persistence setup.
- Persisted mutations resume on their own because their mutationFn is stored with them.
- Mutations with the same key always run one after another.