Offline-First React: TanStack Query + IndexedDB Patterns
Introduction “I added an item in aisle 5, closed the app, and it synced automatically on the bus ride home.” That one sentence describes the behavior users notice and remember: writes that survive crashes, restarts, and flaky networks. This article shows a practical, production-tested 3-step recipe to get durable, replayable optimistic writes using TanStack Query (React Query), Dexie (IndexedDB),…
In the realm of web application development, users often take for granted the seamless persistence of their actions, even when faced with crashes, restarts, or unreliable networks. This article presents a practical, production-tested methodology to achieve just that, utilizing TanStack Query (React Query), Dexie (IndexedDB), and a lightweight outbox.
The crux of the approach revolves around treating TanStack Query as a reactive cache over a local database, ensuring writes are durable before departing the device. Herein lies the essence of the methodology, distilled into three key steps.
1. Persisting the reactive cache to IndexedDB:
* Treat TanStack Query as a reactive cache mirroring the local database.
* Persisting the query client, including queries and paused mutations, to durable storage enables the app to restore a consistent local state upon launch and replay any pending work.
* Employ the PersistQueryClientProvider or the persistQueryClient utilities from @tanstack/react-query-persist-client, pairing them with an asynchronous persister based on IndexedDB or Dexie (alternatively, idb-keyval).
* Important considerations include providing a persister that supports getItem, setItem, and removeItem (or utilizing the provided helpers), setting the gcTime on the QueryClient to be at least as long as the persister's maxAge, and utilizing shouldDehydrateMutation to persist only those paused mutations that can be resumed.
2. Registering mutation defaults for resilient rebuilding:
* Paused mutations hold variables and metadata but lack the closure implementing the mutationFn.
* Upon app restart, TanStack Query endeavors to resume paused mutations by looking up a default mutationFn registered via queryClient.setMutationDefaults(mutationKey, { mutationFn, ...}).
* Best practices dictate using stable, declarative mutation keys (arrays or strings) like [items, "create"] and registering defaults at module load, ensuring they exist before any persisted mutations are resumed.
* Only register mutations for which you can rebuild the mutationFn, employing shouldDehydrateMutation to filter out unsupported pauses.
* Example implementation at app boot can be found in the provided code snippet.
3. Utilizing client-generated UUIDs and a Dexie outbox for idempotency and ordering:
* Idempotency and ordering pose significant challenges in offline writes.
* A reliable strategy involves generating client UUIDs (using crypto.randomUUID() or uuidv4) and stamping them onto new items during onMutate.
* Persist the optimistic change in your Dexie tables immediately, making the local DB the single source of truth.
* Enqueue a durable outbox record in Dexie, containing the UUID, action (create/update/delete), variables, mutationKey, metadata, and the timestamp of creation.
* Display a visible sync state to the user (queued, syncing, or failed) and, upon reconnection or app launch, replay outbox entries in order or sequentially per-key, invoking the server and removing successfully processed items from the outbox.
* A minimal Dexie sketch and mutation flow can be found in the accompanying code block.
By adhering to this three-step recipe, developers can craft applications that offer offline-first experiences, ensuring users' actions are faithfully persisted and seamlessly resumed, irrespective of network conditions or device crashes.
Written by urgent.news from Dev.to's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.