Contents
In brief
A Dev.to piece walks through a production-tested recipe for offline writes in React: TanStack Query as a reactive cache, Dexie on IndexedDB, and a small outbox. The goal is the aisle-five story — add an item, close the app, and sync on the bus ride home. Three pillars: persist the cache and paused mutations, register mutation defaults so they can be rebuilt after a restart, and stamp client UUIDs with an idempotent replay queue.
What happened
Users remember behavior, not architecture: a write that survives a crash, a flaky network, and a killed process. The author treats TanStack Query as a mirror of the local database, not as the only source of truth. The change lands in Dexie first, then the query cache updates, and the server call leaves when connectivity returns. It is closer to a pocket notebook than to a form that lives only while the tab is open: the page survives a dead battery, and you can copy the shopping list into the system once you are home.
Step one persists the reactive cache to IndexedDB with PersistQueryClientProvider and an async persister. On launch the app restores a consistent local view and calls resumePausedMutations. Set gcTime at least as long as the persister maxAge, and use shouldDehydrateMutation so you only keep paused mutations you know how to resume. Otherwise a restart resurrects zombies: a record without a function to replay it.
Step two is queryClient.setMutationDefaults with stable keys such as ['items', 'create']. A persisted mutation keeps variables and metadata, not the mutationFn closure. Register defaults before resume runs, usually by importing the module at app boot. Step three stamps a client UUID in onMutate, writes into Dexie tables immediately, and enqueues an outbox row with action, mutation key, variables, and timestamp. On reconnect the worker drains the queue in order, sends something like an Idempotency-Key header, and deletes succeeded rows.
Why it matters
Optimistic UI without a durable layer lies to the user until the next reload. The network blinks, the tab closes — and the local edit dies with process memory. An outbox plus client UUIDs turn retries into a safe operation: the server can see the same request twice and refuse to create a duplicate if it honors the idempotency key.
The platform draws hard lines too. Background Sync helps in Chrome; iOS Safari does not ship it. So replay on online, on visibility, and at app launch is the baseline, not an optional flourish. With several tabs and no leader election, two workers drain the same outbox; BroadcastChannel or Web Locks keep a single leader. Exponential backoff with jitter covers transient failures; permanent validation errors (typically 4xx) should leave the queue and surface to the user instead of spinning forever.
In practice
The article is a pattern bundle with code sketches, not an SDK for your domain. Before you copy it, decide what is the source of truth for writes — the local database or the query cache.
- Write to Dexie first; let TanStack Query react to local data changes.
- Persist only resumable paused mutations; without defaults they are useless after restart.
- Register setMutationDefaults before resumePausedMutations.
- Stamp a client UUID and enqueue the outbox row in the same user gesture as the optimistic update.
- On the server, honor Idempotency-Key or sequence numbers so retries do not create duplicates.
- For related entities, replay the outbox in order so create arrives before update.
- With multiple tabs, elect one leader to drain the queue.
Takeaway
Durable offline React here rests on three promises: state and queue survive process death in IndexedDB, paused mutations can be rebuilt from defaults, and retries are safe because of client UUIDs and server-side dedupe. The network stays unreliable — but the user keeps working, sees a sync state, and waits for delivery when connectivity returns.



Comments