Skip to main content

Network mode and offline

A phone loses its connection in a lift. What should the device list do? A request that cannot leave the phone will fail, be retried three times with backoff, and end as an error the user did not cause and cannot fix — unless the query knows it is offline and waits instead.

networkMode says how a query or mutation treats the client's belief about the network:

NetworkModeOffline, it…For
NetworkMode.onlinedoes not start; a retry does not continue. It pauses and resumes when the client is online againthe default: anything that needs the internet
NetworkMode.alwaysignores connectivity: fetches, fails and retries as if onlinework that needs no internet — a local database, a device on the home network
NetworkMode.offlineFirstmakes the first attempt anyway, and pauses a retrya transport with a cache of its own that may answer offline

"Offline" is what the client believes, and nothing is installed to tell it: a client with no connectivity source believes it is online forever, and a failed request is simply a failure. Giving it a source is the connectivity guide.

Try it: in the screen below, turn Online off, press Refetch and then Add todo. Under online neither sends anything: the query shows fetchStatus=paused, the mutation isPaused=true. Turn Online back on and both go out by themselves. Switch Network mode to always and repeat — the requests go out with Online off, and this demo's backend answers them: the switch changes only what the client believes, not the network.

Live demoOfflineNetwork modes, paused mutations, and coming back online.~3 MB, runs in your browser; no server involved.

online: a paused query​

Under the default mode, a query that would fetch while offline does not. Its fetchStatus is paused and its result's isPaused is true:

  • with no data yet, it stays a QueryPending — neither loading nor failed;
  • with data, it stays a QuerySuccess showing that data.

Both cases deserve a word on screen, because a spinner that never ends and a list that silently stopped updating look like bugs:

String? offlineNote(QueryResult<List<Device>> devices) => switch (devices) {
QueryPending(isPaused: true) => 'Waiting for a connection…',
QuerySuccess(isPaused: true) => 'Offline — showing the last list loaded',
_ => null,
};

A retry is subject to the same rule: a fetch whose first attempt failed and whose connection then dropped pauses between attempts instead of spending them. When the client is online again, a mounted client continues every paused fetch where it stopped — one request, not a fresh start — and then refetches stale queries that have a reader, per refetchOnReconnect (a RefetchOn, default ifStale).

always: ignore connectivity​

A smart-home app talks to two things: the vendor's cloud account, which needs the internet, and a gateway on the home Wi-Fi, which answers whether or not the router has an uplink. For the gateway, "offline" is the wrong question. Defaults for a key prefix put every gateway query and mutation in always, and leave the cloud ones online:

// The gateway is on the home network: it answers whether or not the phone
// has a route to the internet. The cloud account's queries keep `online`.
void talkToTheGatewayOffline(QueryClient client) {
client.setQueryDefaults(
DeviceKeys.all,
const QueryDefaults(networkMode: NetworkMode.always),
);
client.setMutationDefaults(
DeviceKeys.all,
const MutationDefaults(networkMode: NetworkMode.always),
);
}

Key defaults match by prefix, so every query key under ['devices'] gets them; a mutation gets them only when it carries a mutationKey under ['devices'].

Queries and mutations have separate defaults. A networkMode in the query defaults does not reach mutations, and the other way round; an app that means both sets both, as above — or for the whole client, DefaultOptions(queries: QueryDefaults(networkMode: …), mutations: MutationDefaults(networkMode: …)). Each resolves option → the defaults registered for its key → the client's default → online.

Under always, refetchOnReconnect defaults to never: a query that never waited for the network has nothing to catch up on when it returns.

offlineFirst: try once​

Some transports can answer without the network — an HTTP client with a disk cache, a service worker on the web. For those, pausing before the first attempt would hide data that is there:

// The repository sits behind an HTTP cache that can answer from disk.
QueryObserverOptions<List<DeviceType>> cachedDeviceTypesQuery() =>
QueryObserverOptions(
queryKey: QueryKey(<Object?>['device-types']),
queryFn: (context) => deviceRepository.types(signal: context.signal),
networkMode: NetworkMode.offlineFirst,
);

The first attempt runs even offline. If it fails, the retry after it pauses until the client is online, as under online.

Mutations offline​

In the default online mode, a mutation started offline is not sent and not failed. It is pending with isPaused: true — the UI can show the write as queued — and it runs when the client is online again. Several writes made offline are all started again at once, in the order they were made; to send each only after the one before it has settled, give them a shared scope.

client.resumePausedMutations() is the manual door. A mounted client calls it by itself when it comes back online, so it is rarely needed. It decides per mutation: one that still cannot run (an online mutation while offline) is left where it is, and the returned future does not wait for the network.

A mutation that could run offline (always) but is queued behind a scope-mate that cannot stays paused until that one moves.

Paused work lives in memory

Paused queries and queued mutations belong to the running client. If the operating system ends the app while they wait, they are gone — there is no persistence layer in 1.0. A write that must survive a restart needs a queue of your own, stored on the device.

Resuming needs a mounted client​

Paused work resumes when the client hears that it is online, and it hears only while mounted. QueryClientProvider mounts its client for you; a pure-Dart client is mounted with client.mount(). See the mount contract.

In React Query

networkMode has the same three values and the same meaning, and a paused query reports fetchStatus: 'paused'. The difference is in resumePausedMutations: TanStack Query resumes nothing while offline, while here each paused mutation is decided by its own network mode, so an always mutation is resumed offline. See differences from TanStack Query.