| name | frontend-optimistic-mutations |
| description | A portable, framework-agnostic discipline for the write path of any React or React Native app using a query/cache layer. Codifies the optimistic-update lifecycle (cancel in-flight queries → snapshot |
| category | Document Processing |
| source | antigravity |
| tags | ["react","api","claude","ai","agent","document","security","cro"] |
| url | https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/frontend-optimistic-mutations |
Frontend Optimistic Mutations (the write path)
When to Use
Use this skill when you need a portable, framework-agnostic discipline for the write path of any React or React Native app using a query/cache layer. Codifies the optimistic-update lifecycle (cancel in-flight queries → snapshot every affected cache → patch instantly → roll back verbatim on error → invalidate on...
Portable skill — readable by Claude Code, OpenCode, Codex, Cursor, Windsurf, and others.
This skill describes the discipline of the write path — optimistic updates, rollback,
idempotency, cache coherence — not a UI library or a styling system. It builds directly on the
frontend-data-contracts skill (writes go through the typed client) and the
frontend-architecture skill (mutations live in modules/{feature}/hooks/, keyed by a factory).
The goal: a write feels instant (the UI reflects it before the server confirms), is safe
(a failure restores the exact prior state, and a retry never double-charges), and leaves the cache
coherent (the detail view and every list page agree). All three at once — that's the craft.
0. The five core ideas
- The optimistic lifecycle is fixed. cancel → snapshot → patch → (error: roll back) → (settle: invalidate). Every optimistic mutation follows the same five beats.
- Roll back verbatim. On failure, restore the exact snapshot taken before the patch — not a "best guess" re-derivation. Keep the snapshot in mutation context.
- Idempotency is generated once, not per attempt. The key is created at form init (or first intent), so a network retry replays the original server response instead of performing the action twice.
- Caches move in lock-step. A status change patches the detail cache and every list page that contains the entity, so badges never disagree across surfaces.
- Server state never enters the client store. Optimistic state lives in the query cache, not Zustand/Redux. The cache is the single source of truth for server data (per frontend-architecture §4).
1. When to be optimistic (and when not)
| Situation | Strategy |
|---|
| High-confidence, low-conflict write (toggle status, like, mark-paid, reorder) | Optimistic — patch immediately, roll back on error. |
| Create that returns a server-generated id/number/total | Pending state, then setQueryData from the server response. A temporary optimistic row is optional; reconcile on success. |
| Destructive or hard-to-reverse write (delete with cascade, send money) | Confirm first, then optimistic or pending — never silent-optimistic. |
| Write whose result the user can't see yet (background job) | Pending + toast, invalidate when done. No optimistic patch. |
Optimism is a UX tool for writes you're confident will succeed. If failure is common or expensive to
undo, prefer a pending state.
2. The optimistic lifecycle (TanStack Query)
The canonical shape. Each beat has a job; skipping one breaks correctness.
interface MarkPaidContext {
previousInvoice: Invoice | undefined;
previousLists: Array<[readonly unknown[], InvoiceListResponse]>;
}
export function useMarkInvoicePaid() {
const queryClient = useQueryClient();
const notifyError = useApiErrorToast();
return useMutation<Invoice, ApiError, { id: InvoiceId }, MarkPaidContext>({
mutationFn: ({ id }) => apiClient.post<Invoice>(INVOICE_API.markPaid(id)),
onMutate: async ({ id }) => {
await queryClient.cancelQueries({ queryKey: invoiceKeys.all });
const detailKey = invoiceKeys.detail(id);
const previousInvoice = queryClient.getQueryData<Invoice>(detailKey);
(previousInvoice) {
queryClient.<>(detailKey, {
...previousInvoice,
: .,
});
}
: [] = [];
( [key, list] queryClient.<>(
{
: invoiceKeys.(),
},
)) {
(!list) ;
previousLists.([key, li