frontend-optimistic-mutations – skill Thiết kế & UI cho agent AI
Kỷ luật độc lập framework cho luồng ghi (write path) của mọi ứng dụng React hoặc React Native dùng lớp query/cache.
Cài skill frontend-optimistic-mutations bằng một lệnh
Chọn công cụ bạn đang dùng, copy lệnh và dán vào terminal. Skill sẽ nằm trong thư mục skills của công cụ đó và sẵn sàng dùng ở phiên làm việc tiếp theo.
Claude Code
mkdir -p ~/.claude/skills && curl -fsSL https://skill.dn1nguoi.com/z/frontend-optimistic-mutations-9eebe.tgz | tar xz -C ~/.claude/skills
Gọi trong Claude Code: /frontend-optimistic-mutations – rồi mô tả công việc bạn muốn làm.
Codex
mkdir -p ~/.codex/skills && curl -fsSL https://skill.dn1nguoi.com/z/frontend-optimistic-mutations-9eebe.tgz | tar xz -C ~/.codex/skills
Gọi trong Codex: $frontend-optimistic-mutations – rồi mô tả công việc bạn muốn làm.
Hermes Agent
mkdir -p ~/.hermes/skills && curl -fsSL https://skill.dn1nguoi.com/z/frontend-optimistic-mutations-9eebe.tgz | tar xz -C ~/.hermes/skills
Gọi trong Hermes Agent: /frontend-optimistic-mutations – rồi mô tả công việc bạn muốn làm.
DeepSeek
mkdir -p ~/.deepcode/skills && curl -fsSL https://skill.dn1nguoi.com/z/frontend-optimistic-mutations-9eebe.tgz | tar xz -C ~/.deepcode/skills
Gọi trong DeepSeek: /frontend-optimistic-mutations – rồi mô tả công việc bạn muốn làm.
Nội dung SKILL.md
Nội dung SKILL.md gốc bằng tiếng Anh.
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 reconciles with authoritative state, and retries require server-enforced deduplication), 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
1. The optimistic lifecycle is fixed. cancel → snapshot → patch → (error: roll back) → (settle: invalidate). Every optimistic mutation follows the same five beats.
2. 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.
3. 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.
4. 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.
5. 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 nonfinancial status, like, 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.
// modules/invoice/hooks/useInvoiceMutations.ts
interface MarkPaidContext {
previousInvoice: Invoice | undefined; // detail snapshot
previousLists: Array<[readonly unknown[], InvoiceListResponse]>; // every list page snapshot
}
export function useMarkInvoicePaid() {
const queryClient = useQueryClient();
const notifyError = useApiErrorToast();
return useMutation<Invoice, ApiError, { id: InvoiceId }, MarkPaidContext>({
mutationKey: invoiceKeys.all,
retry: false, // a network error alone does not make this POST safe to retry
mutationFn: ({ id }) => apiClient.post<Invoice>(INVOICE_API.markPaid(id)),
// 1 + 2 + 3: cancel in-flight reads, snapshot, patch
onMutate: async ({ id }) => {
await queryClient.cancelQueries({ queryKey: invoiceKeys.all }); // (1) no late refetch clobber
const detailKey = invoiceKeys.detail(id);
const previousInvoice = queryClient.getQueryData<Invoice>(detailKey); // (2) snapshot detail
if (previousInvoice) {
queryClient.setQueryData<Invoice>(detailKey, {
// (3) patch detail
...previousInvoice,
status: InvoiceStatus.Paid,
});
}
const previousLists: MarkPaidContext["previousLists"] = [];
for (const [key, list] of queryClient.getQueriesData<InvoiceListResponse>(
{
queryKey: invoiceKeys.lists(),
},
)) {
if (!list) continue;
previousLists.push([key, list]); // (2) snapshot each page
if (!list.invoices.some((i) => i.id === id)) continue;
queryClient.setQueryData<InvoiceListResponse>(key, {
// (3) patch matching row
...list,
invoices: list.invoices.map((i) =>
i.id === id ? { ...i, status: InvoiceStatus.Paid } : i,
),
});
}
return { previousInvoice, previousLists };
},
// 4: revalidate on failure; an old whole-list snapshot can erase newer writes.
onError: (error) => {
void queryClient.invalidateQueries({ queryKey: invoiceKeys.all });
notifyError(error);
},
// 5: invalidate so authoritative server state (paidAt, aggregates) refetches
onSettled: (_d, _e, { id }) => {
void queryClient.invalidateQueries({ queryKey: invoiceKeys.detail(id) });
void queryClient.invalidateQueries({ queryKey: invoiceKeys.lists() });
},
});
}Why each beat:
- cancel — without it, a query that was already in flight can resolve _after_ your patch and overwrite the optimistic state.
- snapshot — the only safe rollback source; never reconstruct prior state by hand.
- patch — the instant UX; mutate detail and lists together (§4).
- failure — revalidate and surface the typed ApiError; this concurrent example deliberately does not restore whole-cache snapshots. Use pending UI while authoritative data reloads.
- invalidate on settle — success or failure, refetch so server-computed fields (timestamps, totals) are authoritative. Settle, not just success: a failed write may still have changed server state.
---
Whole-snapshot rollback is safe only when cache writes are serialized or no newer update has occurred. The reference therefore invalidates on failure. Restoring snapshots requires serialized writes or versioned per-entity patches that prove no newer update would be overwritten. Filtered lists may change membership/sort/totals when status changes; merely editing a row is insufficient. Use pending state when those rules cannot be reproduced safely. An optimistic Paid badge does not prove a payment settled.
3. Non-optimistic writes: create with server-owned fields
A create that returns an id/number/total can't be fully optimistic. Run it as a pending mutation and
seed the cache from the response.
export function useCreateInvoice() {
const queryClient = useQueryClient();
const notifyError = useApiErrorToast();
return useMutation<Invoice, ApiError, CreateInvoiceInput>({
mutationFn: ({ document, idempotencyKey }) =>
apiClient.post<Invoice>("/invoices", document, { idempotencyKey }),
onSuccess: (invoice) => {
queryClient.setQueryData(invoiceKeys.detail(invoice.id), invoice); // seed detail
void queryClient.invalidateQueries({ queryKey: invoiceKeys.lists() }); // refresh lists
},
onError: (error) => notifyError(error),
});
}---
4. Cache coherence (lock-step detail + lists)
A single entity appears in many caches: its detail, and every filtered/paginated list page. An
optimistic patch must touch all of them or surfaces disagree. Use a hierarchical key factory
(from frontend-architecture §4.3) so you can target precisely.
export const invoiceKeys = {
all: ["invoices"] as const,
lists: () => [...invoiceKeys.all, "list"] as const,
list: (p: IListParams) => [...invoiceKeys.lists(), p] as const,
detail: (id: InvoiceId) => [...invoiceKeys.all, "detail", id] as const,
} as const;getQueriesData({ queryKey: invoiceKeys.lists() })enumerates every cached list page so you can patch each.invalidateQueries({ queryKey: invoiceKeys.lists() })refreshes them all on settle.detail(id)targets exactly one entity.
Snapshot each page you touch (keyed by its exact query key) so rollback restores every page
verba
… (nội dung đầy đủ có trong gói tải về)
Skill liên quan
- frontend-seo – Hệ thống SEO độc lập framework, dùng được cho mọi frontend React hoặc React…
- frontend-ui-dark-ts – Hệ thống giao diện React theo chủ đề tối hiện đại, dùng Tailwind CSS và Framer Motion…
- frontend-ui-engineering – Xây dựng giao diện đạt chất lượng production. Dùng khi tạo hoặc chỉnh sửa giao diện…
- frutiger-aero – Hướng dẫn triển khai web và app theo phong cách Frutiger Aero. Kích hoạt khi người dùng…
- game-art – Nguyên tắc nghệ thuật game. Lựa chọn phong cách hình ảnh, quy trình tài nguyên, quy…
- game-audio – Nguyên tắc âm thanh game. Thiết kế âm thanh, tích hợp nhạc nền, hệ thống âm thanh thích…
- senior-computer-vision – Skill kỹ thuật thị giác máy tính cho phát hiện đối tượng, phân đoạn ảnh và hệ thống thị…
- game-design – Nguyên tắc thiết kế game. Cấu trúc GDD, cân bằng game, tâm lý người chơi, hệ thống tiến…