skip to content

In TanStack Query v5, after the browser fires an offline event, a newly mounted query neither fetches nor errors and its loading UI never resolves. What state is it in, and how does networkMode change that?

level: seniorimportance: should knowfreq 38%

answer

  1. the queryFn never started
  2. a third fetchStatus value
  3. default mode trusts the online manager
  4. always versus offlineFirst

basics

~20 s

Under the default networkMode 'online', a query that should fetch after an offline event is not started: it is status pending with fetchStatus paused, so isLoading is false. It resumes on reconnect; render isPaused, or pick 'always' or 'offlineFirst'.

solid answer

~40 s

In v5 the default `networkMode: 'online'` only runs a `queryFn` when the `onlineManager` says the device is online. After an `offline` event, a newly mounted query is held back: `status: 'pending'`, `fetchStatus: 'paused'`, `isPaused: true`. Nothing fails, so there is no error; and `isLoading` (`isPending && isFetching`) is false, so a component that checks only `isLoading` and `isError` falls through to rendering `undefined` data, while one keyed to `isPending` spins until the connection returns. When the `online` event fires the query continues. `networkMode: 'always'` ignores connectivity - right when the `queryFn` does not touch the network. `'offlineFirst'` runs the `queryFn` once, which suits a service worker or HTTP cache, and pauses only its retries. Note that v5 starts assuming online and does not read `navigator.onLine`.

go deeper

for a junior

Recall that fetchStatus has a paused value and that isPaused is true when a query wants to fetch but the library believes the device is offline.

for a middle

Explain the three network modes, why isLoading is false while paused, and how a query resumes when the online event fires.

for a senior

Diagnose the stuck or blank screen from the flags, render an explicit offline state, and choose always or offlineFirst for queries that do not need the network.

for a principal

Decide an offline policy for the product: which data must work offline, whether a service worker or persisted cache backs it, and how network modes express that per query.

## The symptom and the state behind it A user loses connectivity; the browser fires its `offline` event. They then navigate to a screen whose query has never loaded. The screen shows neither data nor an error, and the loading UI either spins forever or renders nothing useful. Nothing failed in the network panel, because **no request was sent**. The query is in this state: | Field | Value | |---|---| | `status` | `'pending'` (no data yet) | | `fetchStatus` | `'paused'` | | `isPaused` | `true` | | `isFetching` | `false` | | `isLoading` | `false` (it is `isPending && isFetching`) | | `error` | `null` | The component's rendering logic decides what the user sees: - keyed to **`isPending`** - a spinner that lasts until the connection returns; - keyed to **`isLoading`** then **`isError`** then data - it falls through both checks and renders with `data` undefined, often a crash or an empty list that looks like "no results". ## Why: network modes TanStack Query v5 has a **`networkMode`** option for queries (and mutations), with three values: | Mode | First fetch while offline | Retries while offline | `refetchOnReconnect` default | |---|---|---|---| | `'online'` (default) | not started; `fetchStatus: 'paused'` | paused | `true` | | `'always'` | runs normally and may fail | not paused for connectivity | `false` | | `'offlineFirst'` | runs once | paused after a failure | `true` | In the default **`'online'`** mode the library does not even try a request it believes cannot succeed. It **pauses** the query and, when the connection returns, **continues** it. That continuation is not a refetch; it happens independently of `refetchOnReconnect`. If a fetch was already running when the connection dropped, its retries pause the same way. ## Where "online" comes from The truth source is the **`onlineManager`**. In v5 it: - starts by **assuming the device is online**; - listens to the window's `online` and `offline` events to change its mind; - does **not** read `navigator.onLine`, which v5 dropped because some browsers report false negatives from it. Two consequences follow. A page that loads while already offline is treated as online until an `offline` event fires, so its first fetch is attempted and fails normally. And environments without those window events, such as React Native, should wire their own connectivity signal with `onlineManager.setEventListener(...)`. ## Fixing the UI 1. **Render the paused state explicitly.** Check `isPaused` (or `fetchStatus === 'paused'`) and show "You are offline - this will load when you reconnect", instead of a spinner. 2. **Order the branches safely.** Handle "no data" as a case of its own: loading, paused, error, then data. Never assume that "not loading and not error" means data exists. 3. **Choose the right mode per query:** - **`'always'`** when the `queryFn` does not depend on the network: reading local storage, IndexedDB or computing a value. Offline pausing would only block it. - **`'offlineFirst'`** when a service worker or the HTTP cache may answer offline. The first attempt may succeed from that cache; if it fails, retries pause like `'online'`. - keep **`'online'`** for ordinary API calls; it avoids burning retries and battery while offline. ```tsx import { useQuery } from '@tanstack/react-query' function Inbox() { const inbox = useQuery({ queryKey: ['inbox'], queryFn: fetchInbox }) if (inbox.isPending) { return inbox.isPaused ? <p>Offline. Your inbox will load when the connection returns.</p> : <Spinner /> } if (inbox.isLoadingError) return <ErrorBox error={inbox.error} /> return <MessageList messages={inbox.data} /> } ``` The `isPaused` check sits inside the `isPending` branch on purpose: a query with cached data can also be paused (a refetch waiting for the connection), and then the cached messages should stay visible rather than be replaced by an offline notice. ## Diagnosing it quickly The TanStack Query Devtools show paused queries as their own state and offer a toggle that sets the `onlineManager` offline without touching the real connection - the quickest way to reproduce this bug and check the fix. A short checklist for the incident: - **No request in the network panel** and `fetchStatus: 'paused'` - the query is waiting for the `onlineManager`, not broken. - **Requests that fail and retry** while the user insists they are offline - the `onlineManager` never saw an `offline` event, so it still believes the device is online. - **A query that must work offline but pauses** - its `networkMode` is still the default `'online'`. - **A blank screen instead of a spinner** - the render branches assume data exists whenever `isLoading` and `isError` are both false.

  • The page loaded while the device was already offline. Why did the query error instead of pausing?
    In v5 the `onlineManager` starts with online set to true and only changes on `online`/`offline` events; it does not read `navigator.onLine`. With no `offline` event yet, the query is treated as online, the request is attempted and fails, and the normal retry path runs.
  • Which network mode fits a queryFn that reads IndexedDB, and why?
    `'always'`. The function does not use the network, so pausing it while offline would block data that is available locally. In that mode `refetchOnReconnect` also defaults to false, because reconnecting says nothing about local data.

saying these in an interview costs you the question

  • Assumes a query that cannot reach the network always ends in status error.
  • Treats not isLoading and not isError as proof that data is defined.
  • Believes v5 reads navigator.onLine to decide whether to fetch.
  • Thinks the resume after reconnect depends on refetchOnReconnect being true.
  • Sets networkMode always for a normal API query to hide the paused state.