Data Layer
The unified read/write data layer — query, mutate, reconcile, and invalidate.
Data Layer
Read caching, mutation queuing, ID reconciliation, and cache invalidation are not separate concerns. They are different faces of the same problem: keeping local data consistent with your backend.
The Loop
Query (read from cache or server)
↓
Mutate (write to server, queue if offline)
↓
Reconcile (update local data with server response)
↓
Invalidate (flush stale cached queries)
↓
Query again (fresh data)Every mutation is an implicit invalidation of related reads.
Three Systems
Query — Read Data
Fetch from the server, cache the response by URL + tags, serve from cache when offline. When data changes, invalidation marks cached entries as stale so the next read fetches fresh. Use stale-while-revalidate to serve cached data instantly and refresh in the background.
fetchWithCache(url, { tags: ["todos"] })
↓
Check Cache API (by URL)
↓
Miss → fetch from server → cache response + tags
↓
Hit → return cachedMutate — Write Data
Queue write operations when offline, sync when back online. Capture the server response and use it to reconcile local state. If a mutation exhausts its retries, the optimistic data is automatically rolled back.
queueMutation({ method: "POST", url: "/api/todos", body: {...} })
↓
IndexedDB queue → process when online
↓
Fetch to server → capture response
↓
reconcileRecord() + invalidateByTag()Reconcile + Invalidate — Keep Data Fresh
After a mutation syncs, two things happen:
Reconcile: Replace temporary local IDs with server-assigned IDs, merge server fields, update references in related records.
Invalidate: Delete all cached responses tagged with related tags (e.g., after POST /api/todos, invalidate all cached queries tagged todos). The next read fetches fresh data. The SW also broadcasts TAG_INVALIDATED to all open tabs so they refetch automatically (see Cross-Tab Sync).
reconcileRecord("todos", tempId, serverData)
→ local record updated with server data
→ $synced: true
invalidateByTag("todos")
→ Cache API entries cleared
→ window event: "cache-invalidated"
→ framework adapters react → refetchWindow Events
| Event | Detail | When |
|---|---|---|
cache-invalidated | { tags: string[] } | After mutation sync invalidates cached queries |
mutation-queue-changed | none | Queue added/removed/updated |
mutation-rollback | { method, url, tempId, previousData } | Mutation exhausted retries, optimistic data reverted |
mutation-sync-complete | { succeeded, failed } | After queue drain |
Cross-Tab Flow
When a mutation syncs in one tab, all other tabs with cached data for the same tags refetch automatically:
Tab A: invalidateByTag("todos")
↓
SW clears runtime cache entries tagged "todos"
↓
SW broadcasts TAG_INVALIDATED { tag: "todos" } → Tab A, Tab B, Tab C
↓
Tab A: already dispatched cache-invalidated → refetches
Tab B: initCrossTabSync receives TAG_INVALIDATED → dispatches cache-invalidated → refetches
Tab C: initCrossTabSync receives TAG_INVALIDATED → dispatches cache-invalidated → refetchesSetup requires calling initCrossTabSync() once after SW registration (see API Integration).
Data Flow
Online
- Component calls
fetchWithCache("/api/todos", { tags: ["todos"] }) - SW checks Cache API → miss → fetches from server
- Response cached with tag
todos - User creates a todo →
queueMutation()→ fetch succeeds →reconcileRecord()updates local data →invalidateByTag("todos")clears cached/api/todos→ event dispatched → component refetches
Offline
- Component reads from IndexedDB (app data) or Cache API (SW-cached read)
- User creates a todo →
queueMutation()stores in IndexedDB queue →$synced: false - Component shows optimistic state with pending count
- Back online →
processMutationQueue()→ fetch succeeds →reconcileRecord()→invalidateByTag()→ component refetches
Prerequisites
- Offline-First Architecture — understand the client runtime
- SW Template — foundation of the caching system
- API Integration — fetch wrapper with cache strategy
Next Steps
- API Integration — tag-aware fetch wrapper
- Mutation Queuing — queue, reconcile, invalidate
Swoff