Skip to content

ADR-003: Optimistic Mutations via Factory Hooks

Status

Accepted

Context

Optimistic UI requires the same TanStack Query choreography everywhere: cancel in-flight queries, snapshot the cache, apply the patch, roll back on error, invalidate on settled. Hand-rolling this per mutation duplicated ~40 lines each time and produced subtle inconsistencies (missing cancellation, missing rollback, divergent invalidation).

Decision

Optimistic cache updates go through a four-hook factory family in src/hooks/optimistic/: useOptimisticCreate, useOptimisticUpdate, useOptimisticDelete for single-list CRUD, and the generic useOptimisticPatch<TVariables, TResponse, TData> for any other single-query patch (reorders, toggles, detail-object updates). All share the cancel/snapshot/rollback/invalidate plumbing in optimisticMutationHelpers.ts. Error toasts come from the global MutationCache handler; the hooks roll back silently.

Hand-rolled onMutate/onError implementations remain only for mutations spanning multiple cached queries (marker caches, notes lists, paginated transactions), using the cacheUtils helpers.

Consequences

  • A mutation declares only its patch (applyPatch) and optional server re-patch (applyResponse); rollback and invalidation are implemented once
  • mutationKey support preserves isMutating guards used by drag-and-drop reorder UIs
  • Multi-query mutations stay bespoke by design — forcing them through the factory would make the interface as complex as the implementations it hides