Make offline behavior explicit
When an app must keep working offline or restore data after a reload, choose each query's network mode from where its data comes from, show users when work is paused, and persist only versioned, non-sensitive cache entries.
Implementation
- Keep the default
networkMode: 'online'for queries that need the network; they pause while offline. - Use
networkMode: 'always'for query functions that read local data or fall back to it, so they run offline. - Use
networkMode: 'offlineFirst'when a service worker or HTTP cache may answer without a connection. - Show paused work: a query with
fetchStatus === 'paused'is waiting for the network, and a mutation state withisPausedis queued. - When persisting the cache:
- Use
PersistQueryClientProviderwithcreateAsyncStoragePersister; the sync storage persister is deprecated. - Set
gcTimeto at least the persister'smaxAge, or entries are removed before they can be restored. - Set
busterto the app or schema version, so a release with a new data shape discards the old cache. - Use
dehydrateOptions.shouldDehydrateQueryto exclude sensitive or rapidly changing data. - Use
useIsRestoringto wait for restoration before depending on restored data.
- Use
Rationale
By default, TanStack Query pauses fetches and mutations while the browser reports being offline, which looks like an endless loading state unless the UI says so. Query functions that do not need the network should not pause. A persisted cache outlives the code that wrote it and is stored on the device, so it needs versioning and filtering.
Examples
Application: Showing paused work
Incorrect (counterexample):
const pending = useMutationState({ filters: { status: 'pending' } });
const paused = pending.filter((mutation) => mutation.state.isPaused);
Without select, useMutationState already returns each mutation's state, so mutation.state is undefined and this throws.
Correct:
const pausedCount = useMutationState({
filters: { status: 'pending' },
select: (mutation) => mutation.state.isPaused,
}).filter(Boolean).length;
Application: Persisting the cache
Correct:
const queryClient = new QueryClient({
defaultOptions: { queries: { gcTime: 24 * 60 * 60 * 1000 } },
});
const persister = createAsyncStoragePersister({ storage: window.localStorage });
<PersistQueryClientProvider
client={queryClient}
persistOptions={{
persister,
maxAge: 24 * 60 * 60 * 1000,
buster: APP_VERSION,
dehydrateOptions: {
shouldDehydrateQuery: (query) => query.state.status === 'success' && query.queryKey[0] !== 'session',
},
}}
>
<App />
</PersistQueryClientProvider>;
Validation
Switch the browser to offline mode and check that paused queries and mutations are visible to the user and resume when back online. Reload with a persisted cache from an older app version and check that it is discarded. Inspect stored data for sensitive fields.
An app that does not need offline support and keeps the defaults is not a violation.