[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6P3M57BM8V0H3TYJRF0W97P":3,"skill-same:01M6P3M57BM8V0H3TYJRF0W97P":71},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":22,"publishedAt":43,"linkOnly":44,"markdownContent":45,"markdownBody":46,"frontMatter":47,"refName":35,"commitSha":52,"skillDirectory":53,"forksCount":13,"repositoryHtmlUrl":54,"repositoryHomepageUrl":6,"ownerHtmlUrl":55,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":56,"install":57,"resources":70},"01M6P3M57BM8V0H3TYJRF0W97P","frontend-optimistic-mutations",null,"A portable, framework-agnostic discipline for the write path of any React or React Native app using a query\u002Fcache layer.","duc-duy-1612\u002FOnline-Delivery-Systems-Web","duc-duy-1612","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F192231384?v=4","01M6NSDKQQDKVTHZ8DZMZ1VYY6","01M6NSDKW8DDVTQHBT1HBDDJPT",0,"MIT","https:\u002F\u002Fgithub.com\u002Fduc-duy-1612\u002FOnline-Delivery-Systems-Web\u002Fblob\u002Fmain\u002F.agents\u002Fskills_archive\u002Ffrontend-optimistic-mutations\u002FSKILL.md",[17],{"categoryNo":18,"categoryName":19,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000009","前端开发","SCENARIO","使用场景",[23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42],"frontend optimistic mutations","mutations","frontend","wed","for","path","layer","tree","native","critical","app","query","main","community","discipline","react native","write","framework","architecture","react","2026-11-10 10:12:30",false,"---\nname: frontend-optimistic-mutations\ndescription: \"A portable, framework-agnostic discipline for the write path of any React or React Native app using a query\u002Fcache layer.\"\nrisk: critical\nsource: https:\u002F\u002Fgithub.com\u002Fstareezy-1\u002Ffrontend-architecture-skill\u002Ftree\u002Fmain\u002Fskills\u002Ffrontend-optimistic-mutations\nsource_repo: stareezy-1\u002Ffrontend-architecture-skill\nsource_type: community\ndate_added: 2026-07-01\nlicense: MIT\nlicense_source: https:\u002F\u002Fgithub.com\u002Fstareezy-1\u002Ffrontend-architecture-skill\u002Fblob\u002Fmain\u002FLICENSE\n---\n\n# Frontend Optimistic Mutations (the write path)\n## When to Use\n\nUse this skill when you need a portable, framework-agnostic discipline for the write path of any React or React Native app using a query\u002Fcache layer. Codifies the optimistic-update lifecycle (cancel in-flight queries → snapshot every affected cache → patch instantly → roll back verbatim on error → invalidate on...\n\n\n> Portable skill — readable by Claude Code, OpenCode, Codex, Cursor, Windsurf, and others.\n> This skill describes the **discipline of the write path** — optimistic updates, rollback,\n> idempotency, cache coherence — not a UI library or a styling system. It builds directly on the\n> **frontend-data-contracts** skill (writes go through the typed client) and the\n> **frontend-architecture** skill (mutations live in `modules\u002F{feature}\u002Fhooks\u002F`, keyed by a factory).\n\nThe goal: a write **feels instant** (the UI reflects it before the server confirms), is **safe**\n(a failure restores the exact prior state, and a retry never double-charges), and leaves the cache\n**coherent** (the detail view and every list page agree). All three at once — that's the craft.\n\n---\n\n## 0. The five core ideas\n\n1. **The optimistic lifecycle is fixed.** cancel → snapshot → patch → (error: roll back) → (settle: invalidate). Every optimistic mutation follows the same five beats.\n2. **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.\n3. **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.\n4. **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.\n5. **Server state never enters the client store.** Optimistic state lives in the query cache, not Zustand\u002FRedux. The cache is the single source of truth for server data (per frontend-architecture §4).\n\n---\n\n## 1. When to be optimistic (and when not)\n\n| Situation                                                                     | Strategy                                                                                                                       |\n| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |\n| High-confidence, low-conflict write (toggle status, like, mark-paid, reorder) | **Optimistic** — patch immediately, roll back on error.                                                                        |\n| Create that returns a server-generated id\u002Fnumber\u002Ftotal                        | **Pending state**, then `setQueryData` from the server response. A temporary optimistic row is optional; reconcile on success. |\n| Destructive or hard-to-reverse write (delete with cascade, send money)        | **Confirm first**, then optimistic _or_ pending — never silent-optimistic.                                                     |\n| Write whose result the user can't see yet (background job)                    | **Pending + toast**, invalidate when done. No optimistic patch.                                                                |\n\nOptimism is a UX tool for writes you're confident will succeed. If failure is common or expensive to\nundo, prefer a pending state.\n\n---\n\n## 2. The optimistic lifecycle (TanStack Query)\n\nThe canonical shape. Each beat has a job; skipping one breaks correctness.\n\n```ts\n\u002F\u002F modules\u002Finvoice\u002Fhooks\u002FuseInvoiceMutations.ts\ninterface MarkPaidContext {\n  previousInvoice: Invoice | undefined; \u002F\u002F detail snapshot\n  previousLists: Array\u003C[readonly unknown[], InvoiceListResponse]>; \u002F\u002F every list page snapshot\n}\n\nexport function useMarkInvoicePaid() {\n  const queryClient = useQueryClient();\n  const notifyError = useApiErrorToast();\n\n  return useMutation\u003CInvoice, ApiError, { id: InvoiceId }, MarkPaidContext>({\n    mutationFn: ({ id }) => apiClient.post\u003CInvoice>(INVOICE_API.markPaid(id)),\n\n    \u002F\u002F 1 + 2 + 3: cancel in-flight reads, snapshot, patch\n    onMutate: async ({ id }) => {\n      await queryClient.cancelQueries({ queryKey: invoiceKeys.all }); \u002F\u002F (1) no late refetch clobber\n\n      const detailKey = invoiceKeys.detail(id);\n      const previousInvoice = queryClient.getQueryData\u003CInvoice>(detailKey); \u002F\u002F (2) snapshot detail\n      if (previousInvoice) {\n        queryClient.setQueryData\u003CInvoice>(detailKey, {\n          \u002F\u002F (3) patch detail\n          ...previousInvoice,\n          status: InvoiceStatus.Paid,\n        });\n      }\n\n      const previousLists: MarkPaidContext[\"previousLists\"] = [];\n      for (const [key, list] of queryClient.getQueriesData\u003CInvoiceListResponse>(\n        {\n          queryKey: invoiceKeys.lists(),\n        },\n      )) {\n        if (!list) continue;\n        previousLists.push([key, list]); \u002F\u002F (2) snapshot each page\n        if (!list.invoices.some((i) => i.id === id)) continue;\n        queryClient.setQueryData\u003CInvoiceListResponse>(key, {\n          \u002F\u002F (3) patch matching row\n          ...list,\n          invoices: list.invoices.map((i) =>\n            i.id === id ? { ...i, status: InvoiceStatus.Paid } : i,\n          ),\n        });\n      }\n      return { previousInvoice, previousLists };\n    },\n\n    \u002F\u002F 4: roll back verbatim\n    onError: (error, { id }, ctx) => {\n      if (ctx?.previousInvoice)\n        queryClient.setQueryData(invoiceKeys.detail(id), ctx.previousInvoice);\n      for (const [key, list] of ctx?.previousLists ?? [])\n        queryClient.setQueryData(key, list);\n      notifyError(error);\n    },\n\n    \u002F\u002F 5: invalidate so authoritative server state (paidAt, aggregates) refetches\n    onSettled: (_d, _e, { id }) => {\n      void queryClient.invalidateQueries({ queryKey: invoiceKeys.detail(id) });\n      void queryClient.invalidateQueries({ queryKey: invoiceKeys.lists() });\n    },\n  });\n}\n```\n\n**Why each beat:**\n\n- **cancel** — without it, a query that was already in flight can resolve _after_ your patch and overwrite the optimistic state.\n- **snapshot** — the only safe rollback source; never reconstruct prior state by hand.\n- **patch** — the instant UX; mutate detail **and** lists together (§4).\n- **roll back** — restore snapshots verbatim, then surface the typed `ApiError`.\n- **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.\n\n---\n\n## 3. Non-optimistic writes: create with server-owned fields\n\nA create that returns an id\u002Fnumber\u002Ftotal can't be fully optimistic. Run it as a pending mutation and\nseed the cache from the response.\n\n```ts\nexport function useCreateInvoice() {\n  const queryClient = useQueryClient();\n  return useMutation\u003CInvoice, ApiError, CreateInvoiceInput>({\n    mutationFn: ({ document, idempotencyKey }) =>\n      apiClient.post\u003CInvoice>(\"\u002Finvoices\", document, { idempotencyKey }),\n    onSuccess: (invoice) => {\n      queryClient.setQueryData(invoiceKeys.detail(invoice.id), invoice); \u002F\u002F seed detail\n      void queryClient.invalidateQueries({ queryKey: invoiceKeys.lists() }); \u002F\u002F refresh lists\n    },\n    onError: (error) => notifyError(error),\n  });\n}\n```\n\n---\n\n## 4. Cache coherence (lock-step detail + lists)\n\nA single entity appears in many caches: its detail, and every filtered\u002Fpaginated list page. An\noptimistic patch must touch **all of them** or surfaces disagree. Use a **hierarchical key factory**\n(from frontend-architecture §4.3) so you can target precisely.\n\n```ts\nexport const invoiceKeys = {\n  all: [\"invoices\"] as const,\n  lists: () => [...invoiceKeys.all, \"list\"] as const,\n  list: (p: IListParams) => [...invoiceKeys.lists(), p] as const,\n  detail: (id: InvoiceId) => [...invoiceKeys.all, \"detail\", id] as const,\n} as const;\n```\n\n- `getQueriesData({ queryKey: invoiceKeys.lists() })` enumerates **every** cached list page so you can patch each.\n- `invalidateQueries({ queryKey: invoiceKeys.lists() })` refreshes them all on settle.\n- `detail(id)` targets exactly one entity.\n\nSnapshot **each** page you touch (keyed by its exact query key) so rollback restores every page\nverbatim, not just the one currently on screen.\n\n---\n\n## 5. Idempotency (safe retries on money-moving writes)\n\nA retried POST must not perform the action twice. Generate the key **once, at form init** (or first\nuser intent), carry it through retries, and let the client send it as a header. The server replays\nthe original response for a repeated key within its window.\n\n```ts\n\u002F\u002F at form initialisation — stable for the lifetime of this attempt\nconst idempotencyKey = useMemo(() => crypto.randomUUID(), []);\n\n\u002F\u002F mutation forwards it; the typed client puts it on the header\napiClient.post\u003CInvoice>(\"\u002Finvoices\", document, { idempotencyKey });\n```\n\n**Hard rules:**\n\n- Generate the key at **intent time**, not inside `mutationFn` (which re-runs per retry → defeats the purpose).\n- The data client auto-detects financial routes and injects the header; an explicit key always wins so retries replay.\n- Pair idempotency with **disabled-while-pending** UI so the user can't fire a second distinct write.\n\n---\n\n## 6. Retry policy\n\n- **Reads:** retry a few times with backoff (default in most query libs) — safe and idempotent.\n- **Writes:** do **not** auto-retry non-idempotent mutations. Retry only when an idempotency key guarantees replay, or only on network errors (status 0), never on 4xx.\n- **Conflicts (409):** don't retry — surface the typed error, invalidate, and let the user re-decide on fresh data.\n\n```ts\nuseMutation({\n  retry: (count, error: ApiError) => error.isNetworkError && count \u003C 2, \u002F\u002F network-only, bounded\n});\n```\n\n---\n\n## 7. Library adapters\n\nThe five-beat lifecycle is the same; the hooks differ.\n\n| Library            | Optimistic mechanism                                                                                                                                       |\n| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **TanStack Query** | `onMutate` (cancel + snapshot + patch) → `onError` (rollback) → `onSettled` (invalidate). The reference shape above.                                       |\n| **RTK Query**      | `onQueryStarted`: `updateQueryData` returns a `patchResult`; `await queryFulfilled` and call `patchResult.undo()` in `catch`. `invalidatesTags` on settle. |\n| **SWR**            | `mutate(key, optimisticData, { rollbackOnError: true, populateCache, revalidate: true })` — optimistic data + automatic rollback + revalidate.             |\n\nFor **React Native**, all three libraries work unchanged; the cache is the source of truth on\nnative too. Keep mutation hooks DOM-free so they're shareable across web and native.\n\n---\n\n## 8. Conventions checklist (enforce in review)\n\n- [ ] Optimistic mutations follow cancel → snapshot → patch → rollback → invalidate.\n- [ ] `onMutate` cancels in-flight queries before patching.\n- [ ] Rollback restores the **exact** snapshot from context, not a re-derivation.\n- [ ] Detail **and** every affected list page are patched and snapshotted together.\n- [ ] `onSettled` invalidates so server-computed fields are refetched (on success _and_ error).\n- [ ] Idempotency key is generated at intent time and replayed across retries, not regenerated per attempt.\n- [ ] Money-moving \u002F destructive writes confirm first and disable the trigger while pending.\n- [ ] Non-idempotent writes don't auto-retry; 409s surface rather than retry.\n- [ ] Server state stays in the query cache — never copied into a client store.\n- [ ] Query keys come from a hierarchical factory; invalidation is scoped, not global-blunt.\n\n---\n\n## 9. How to apply this skill\n\n**Adding an optimistic mutation:** decide it's safe to be optimistic (§1). Write the five beats\n(§2). Identify every cache the entity lives in and patch\u002Fsnapshot all of them (§4).\n\n**Making a write safe to retry:** generate an idempotency key at form init, thread it through the\nmutation, confirm the client sends it (§5), and set a network-only bounded retry (§6).\n\n**Debugging a flicker \u002F wrong-state-after-write:** check that `onMutate` cancels queries (late\nrefetch clobber) and that `onSettled` invalidates (stale server-computed fields). Check that _all_\nlist pages were patched, not just the visible one.\n\n**Reviewing the write path:** run the checklist in §8. The highest-value catches are missing\n`cancelQueries` (race clobber), partial cache patches (detail\u002Flist disagreement), and idempotency\nkeys generated inside `mutationFn` (no longer protect retries).\n\n---\n\n## Publishing \u002F installing this skill\n\nThis skill follows the Anthropic `SKILL.md` format and is portable across agents.\n\n1. Keep it under `skills\u002Ffrontend-optimistic-mutations\u002FSKILL.md` in a public GitHub repo.\n2. Keep the frontmatter `name` and high-signal `description` — discovery indexes match against it.\n3. Install with: `npx skills add \u003Corg>\u002F\u003Crepo> --skill \"frontend-optimistic-mutations\"`.\n4. Non-`SKILL.md` agents can be pointed here from `AGENTS.md` \u002F `CLAUDE.md`; Kiro can mirror it as a steering file.\n\n## Limitations\n\n- Use this skill only when the task clearly matches its upstream source and local project context.\n- Verify commands, generated code, dependencies, credentials, and external service behavior before applying changes.\n- Do not treat examples as a substitute for environment-specific tests, security review, or user approval for destructive or costly actions.\n","# Frontend Optimistic Mutations (the write path)\n## When to Use\n\nUse this skill when you need a portable, framework-agnostic discipline for the write path of any React or React Native app using a query\u002Fcache layer. Codifies the optimistic-update lifecycle (cancel in-flight queries → snapshot every affected cache → patch instantly → roll back verbatim on error → invalidate on...\n\n\n> Portable skill — readable by Claude Code, OpenCode, Codex, Cursor, Windsurf, and others.\n> This skill describes the **discipline of the write path** — optimistic updates, rollback,\n> idempotency, cache coherence — not a UI library or a styling system. It builds directly on the\n> **frontend-data-contracts** skill (writes go through the typed client) and the\n> **frontend-architecture** skill (mutations live in `modules\u002F{feature}\u002Fhooks\u002F`, keyed by a factory).\n\nThe goal: a write **feels instant** (the UI reflects it before the server confirms), is **safe**\n(a failure restores the exact prior state, and a retry never double-charges), and leaves the cache\n**coherent** (the detail view and every list page agree). All three at once — that's the craft.\n\n---\n\n## 0. The five core ideas\n\n1. **The optimistic lifecycle is fixed.** cancel → snapshot → patch → (error: roll back) → (settle: invalidate). Every optimistic mutation follows the same five beats.\n2. **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.\n3. **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.\n4. **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.\n5. **Server state never enters the client store.** Optimistic state lives in the query cache, not Zustand\u002FRedux. The cache is the single source of truth for server data (per frontend-architecture §4).\n\n---\n\n## 1. When to be optimistic (and when not)\n\n| Situation                                                                     | Strategy                                                                                                                       |\n| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |\n| High-confidence, low-conflict write (toggle status, like, mark-paid, reorder) | **Optimistic** — patch immediately, roll back on error.                                                                        |\n| Create that returns a server-generated id\u002Fnumber\u002Ftotal                        | **Pending state**, then `setQueryData` from the server response. A temporary optimistic row is optional; reconcile on success. |\n| Destructive or hard-to-reverse write (delete with cascade, send money)        | **Confirm first**, then optimistic _or_ pending — never silent-optimistic.                                                     |\n| Write whose result the user can't see yet (background job)                    | **Pending + toast**, invalidate when done. No optimistic patch.                                                                |\n\nOptimism is a UX tool for writes you're confident will succeed. If failure is common or expensive to\nundo, prefer a pending state.\n\n---\n\n## 2. The optimistic lifecycle (TanStack Query)\n\nThe canonical shape. Each beat has a job; skipping one breaks correctness.\n\n```ts\n\u002F\u002F modules\u002Finvoice\u002Fhooks\u002FuseInvoiceMutations.ts\ninterface MarkPaidContext {\n  previousInvoice: Invoice | undefined; \u002F\u002F detail snapshot\n  previousLists: Array\u003C[readonly unknown[], InvoiceListResponse]>; \u002F\u002F every list page snapshot\n}\n\nexport function useMarkInvoicePaid() {\n  const queryClient = useQueryClient();\n  const notifyError = useApiErrorToast();\n\n  return useMutation\u003CInvoice, ApiError, { id: InvoiceId }, MarkPaidContext>({\n    mutationFn: ({ id }) => apiClient.post\u003CInvoice>(INVOICE_API.markPaid(id)),\n\n    \u002F\u002F 1 + 2 + 3: cancel in-flight reads, snapshot, patch\n    onMutate: async ({ id }) => {\n      await queryClient.cancelQueries({ queryKey: invoiceKeys.all }); \u002F\u002F (1) no late refetch clobber\n\n      const detailKey = invoiceKeys.detail(id);\n      const previousInvoice = queryClient.getQueryData\u003CInvoice>(detailKey); \u002F\u002F (2) snapshot detail\n      if (previousInvoice) {\n        queryClient.setQueryData\u003CInvoice>(detailKey, {\n          \u002F\u002F (3) patch detail\n          ...previousInvoice,\n          status: InvoiceStatus.Paid,\n        });\n      }\n\n      const previousLists: MarkPaidContext[\"previousLists\"] = [];\n      for (const [key, list] of queryClient.getQueriesData\u003CInvoiceListResponse>(\n        {\n          queryKey: invoiceKeys.lists(),\n        },\n      )) {\n        if (!list) continue;\n        previousLists.push([key, list]); \u002F\u002F (2) snapshot each page\n        if (!list.invoices.some((i) => i.id === id)) continue;\n        queryClient.setQueryData\u003CInvoiceListResponse>(key, {\n          \u002F\u002F (3) patch matching row\n          ...list,\n          invoices: list.invoices.map((i) =>\n            i.id === id ? { ...i, status: InvoiceStatus.Paid } : i,\n          ),\n        });\n      }\n      return { previousInvoice, previousLists };\n    },\n\n    \u002F\u002F 4: roll back verbatim\n    onError: (error, { id }, ctx) => {\n      if (ctx?.previousInvoice)\n        queryClient.setQueryData(invoiceKeys.detail(id), ctx.previousInvoice);\n      for (const [key, list] of ctx?.previousLists ?? [])\n        queryClient.setQueryData(key, list);\n      notifyError(error);\n    },\n\n    \u002F\u002F 5: invalidate so authoritative server state (paidAt, aggregates) refetches\n    onSettled: (_d, _e, { id }) => {\n      void queryClient.invalidateQueries({ queryKey: invoiceKeys.detail(id) });\n      void queryClient.invalidateQueries({ queryKey: invoiceKeys.lists() });\n    },\n  });\n}\n```\n\n**Why each beat:**\n\n- **cancel** — without it, a query that was already in flight can resolve _after_ your patch and overwrite the optimistic state.\n- **snapshot** — the only safe rollback source; never reconstruct prior state by hand.\n- **patch** — the instant UX; mutate detail **and** lists together (§4).\n- **roll back** — restore snapshots verbatim, then surface the typed `ApiError`.\n- **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.\n\n---\n\n## 3. Non-optimistic writes: create with server-owned fields\n\nA create that returns an id\u002Fnumber\u002Ftotal can't be fully optimistic. Run it as a pending mutation and\nseed the cache from the response.\n\n```ts\nexport function useCreateInvoice() {\n  const queryClient = useQueryClient();\n  return useMutation\u003CInvoice, ApiError, CreateInvoiceInput>({\n    mutationFn: ({ document, idempotencyKey }) =>\n      apiClient.post\u003CInvoice>(\"\u002Finvoices\", document, { idempotencyKey }),\n    onSuccess: (invoice) => {\n      queryClient.setQueryData(invoiceKeys.detail(invoice.id), invoice); \u002F\u002F seed detail\n      void queryClient.invalidateQueries({ queryKey: invoiceKeys.lists() }); \u002F\u002F refresh lists\n    },\n    onError: (error) => notifyError(error),\n  });\n}\n```\n\n---\n\n## 4. Cache coherence (lock-step detail + lists)\n\nA single entity appears in many caches: its detail, and every filtered\u002Fpaginated list page. An\noptimistic patch must touch **all of them** or surfaces disagree. Use a **hierarchical key factory**\n(from frontend-architecture §4.3) so you can target precisely.\n\n```ts\nexport const invoiceKeys = {\n  all: [\"invoices\"] as const,\n  lists: () => [...invoiceKeys.all, \"list\"] as const,\n  list: (p: IListParams) => [...invoiceKeys.lists(), p] as const,\n  detail: (id: InvoiceId) => [...invoiceKeys.all, \"detail\", id] as const,\n} as const;\n```\n\n- `getQueriesData({ queryKey: invoiceKeys.lists() })` enumerates **every** cached list page so you can patch each.\n- `invalidateQueries({ queryKey: invoiceKeys.lists() })` refreshes them all on settle.\n- `detail(id)` targets exactly one entity.\n\nSnapshot **each** page you touch (keyed by its exact query key) so rollback restores every page\nverbatim, not just the one currently on screen.\n\n---\n\n## 5. Idempotency (safe retries on money-moving writes)\n\nA retried POST must not perform the action twice. Generate the key **once, at form init** (or first\nuser intent), carry it through retries, and let the client send it as a header. The server replays\nthe original response for a repeated key within its window.\n\n```ts\n\u002F\u002F at form initialisation — stable for the lifetime of this attempt\nconst idempotencyKey = useMemo(() => crypto.randomUUID(), []);\n\n\u002F\u002F mutation forwards it; the typed client puts it on the header\napiClient.post\u003CInvoice>(\"\u002Finvoices\", document, { idempotencyKey });\n```\n\n**Hard rules:**\n\n- Generate the key at **intent time**, not inside `mutationFn` (which re-runs per retry → defeats the purpose).\n- The data client auto-detects financial routes and injects the header; an explicit key always wins so retries replay.\n- Pair idempotency with **disabled-while-pending** UI so the user can't fire a second distinct write.\n\n---\n\n## 6. Retry policy\n\n- **Reads:** retry a few times with backoff (default in most query libs) — safe and idempotent.\n- **Writes:** do **not** auto-retry non-idempotent mutations. Retry only when an idempotency key guarantees replay, or only on network errors (status 0), never on 4xx.\n- **Conflicts (409):** don't retry — surface the typed error, invalidate, and let the user re-decide on fresh data.\n\n```ts\nuseMutation({\n  retry: (count, error: ApiError) => error.isNetworkError && count \u003C 2, \u002F\u002F network-only, bounded\n});\n```\n\n---\n\n## 7. Library adapters\n\nThe five-beat lifecycle is the same; the hooks differ.\n\n| Library            | Optimistic mechanism                                                                                                                                       |\n| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **TanStack Query** | `onMutate` (cancel + snapshot + patch) → `onError` (rollback) → `onSettled` (invalidate). The reference shape above.                                       |\n| **RTK Query**      | `onQueryStarted`: `updateQueryData` returns a `patchResult`; `await queryFulfilled` and call `patchResult.undo()` in `catch`. `invalidatesTags` on settle. |\n| **SWR**            | `mutate(key, optimisticData, { rollbackOnError: true, populateCache, revalidate: true })` — optimistic data + automatic rollback + revalidate.             |\n\nFor **React Native**, all three libraries work unchanged; the cache is the source of truth on\nnative too. Keep mutation hooks DOM-free so they're shareable across web and native.\n\n---\n\n## 8. Conventions checklist (enforce in review)\n\n- [ ] Optimistic mutations follow cancel → snapshot → patch → rollback → invalidate.\n- [ ] `onMutate` cancels in-flight queries before patching.\n- [ ] Rollback restores the **exact** snapshot from context, not a re-derivation.\n- [ ] Detail **and** every affected list page are patched and snapshotted together.\n- [ ] `onSettled` invalidates so server-computed fields are refetched (on success _and_ error).\n- [ ] Idempotency key is generated at intent time and replayed across retries, not regenerated per attempt.\n- [ ] Money-moving \u002F destructive writes confirm first and disable the trigger while pending.\n- [ ] Non-idempotent writes don't auto-retry; 409s surface rather than retry.\n- [ ] Server state stays in the query cache — never copied into a client store.\n- [ ] Query keys come from a hierarchical factory; invalidation is scoped, not global-blunt.\n\n---\n\n## 9. How to apply this skill\n\n**Adding an optimistic mutation:** decide it's safe to be optimistic (§1). Write the five beats\n(§2). Identify every cache the entity lives in and patch\u002Fsnapshot all of them (§4).\n\n**Making a write safe to retry:** generate an idempotency key at form init, thread it through the\nmutation, confirm the client sends it (§5), and set a network-only bounded retry (§6).\n\n**Debugging a flicker \u002F wrong-state-after-write:** check that `onMutate` cancels queries (late\nrefetch clobber) and that `onSettled` invalidates (stale server-computed fields). Check that _all_\nlist pages were patched, not just the visible one.\n\n**Reviewing the write path:** run the checklist in §8. The highest-value catches are missing\n`cancelQueries` (race clobber), partial cache patches (detail\u002Flist disagreement), and idempotency\nkeys generated inside `mutationFn` (no longer protect retries).\n\n---\n\n## Publishing \u002F installing this skill\n\nThis skill follows the Anthropic `SKILL.md` format and is portable across agents.\n\n1. Keep it under `skills\u002Ffrontend-optimistic-mutations\u002FSKILL.md` in a public GitHub repo.\n2. Keep the frontmatter `name` and high-signal `description` — discovery indexes match against it.\n3. Install with: `npx skills add \u003Corg>\u002F\u003Crepo> --skill \"frontend-optimistic-mutations\"`.\n4. Non-`SKILL.md` agents can be pointed here from `AGENTS.md` \u002F `CLAUDE.md`; Kiro can mirror it as a steering file.\n\n## Limitations\n\n- Use this skill only when the task clearly matches its upstream source and local project context.\n- Verify commands, generated code, dependencies, credentials, and external service behavior before applying changes.\n- Do not treat examples as a substitute for environment-specific tests, security review, or user approval for destructive or costly actions.\n",{"name":5,"risk":32,"source":48,"license":14,"date_added":49,"description":7,"source_repo":50,"source_type":36,"license_source":51},"https:\u002F\u002Fgithub.com\u002Fstareezy-1\u002Ffrontend-architecture-skill\u002Ftree\u002Fmain\u002Fskills\u002Ffrontend-optimistic-mutations",1782864000000,"stareezy-1\u002Ffrontend-architecture-skill","https:\u002F\u002Fgithub.com\u002Fstareezy-1\u002Ffrontend-architecture-skill\u002Fblob\u002Fmain\u002FLICENSE","f3312ba749561b6159f477dbfc9313c875d40a56",".agents\u002Fskills_archive\u002Ffrontend-optimistic-mutations","https:\u002F\u002Fgithub.com\u002Fduc-duy-1612\u002FOnline-Delivery-Systems-Web","https:\u002F\u002Fgithub.com\u002Fduc-duy-1612","2026-10-25 23:50:58",{"source":8,"skillName":5,"commands":58,"prompt":68,"zipAvailable":69},[59,62,65],{"tool":60,"command":61},"npx","npx skills add duc-duy-1612\u002FOnline-Delivery-Systems-Web --skill frontend-optimistic-mutations",{"tool":63,"command":64},"bunx","bunx skills add duc-duy-1612\u002FOnline-Delivery-Systems-Web --skill frontend-optimistic-mutations",{"tool":66,"command":67},"pnpm","pnpm dlx skills add duc-duy-1612\u002FOnline-Delivery-Systems-Web --skill frontend-optimistic-mutations","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fduc-duy-1612\u002FOnline-Delivery-Systems-Web\u002Fblob\u002Fmain\u002F.agents\u002Fskills_archive\u002Ffrontend-optimistic-mutations\u002FSKILL.md",true,[],[72,97,120,145,173,197],{"skillNo":73,"name":74,"nameZh":6,"description":75,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":76,"categories":77,"keywords":81,"publishedAt":43},"01M6Q0FA5FZ410Z33Z3TFQ4DBA","zero-trust","Implement zero-trust network architecture. Configure identity-based access, micro-segmentation, and continuous verification. Use when implementing modern security architectures.","https:\u002F\u002Fgithub.com\u002Fduc-duy-1612\u002FOnline-Delivery-Systems-Web\u002Fblob\u002Fmain\u002F.agents\u002Fskills_archive\u002Fzero-trust\u002FSKILL.md",[78],{"categoryNo":79,"categoryName":80,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000011","运维与部署",[82,83,84,41,85,86,74,87,88,89,90,91,36,35,92,32,93,94,95,96],"ZeRO","trust","zero trust","assessment","verification","segmentation","devops-security","access","configure","only","active","scope","scripts","relevant","implementing",{"skillNo":98,"name":99,"nameZh":6,"description":100,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":101,"categories":102,"keywords":103,"publishedAt":43},"01M6Q0F1YZJW3KX5FBKEVZ8CV4","zcode-delegate","Delegate coding tasks to the Z.AI ZCode CLI only when the user explicitly requests it, while the orchestrator retains review and landing responsibility.","https:\u002F\u002Fgithub.com\u002Fduc-duy-1612\u002FOnline-Delivery-Systems-Web\u002Fblob\u002Fmain\u002F.agents\u002Fskills_archive\u002Fzcode-delegate\u002FSKILL.md",[],[104,105,106,107,108,109,110,111,112,113,114,115,116,91,117,36,118,33,32,119],"delegate","zcode","zcode delegate","review","cli","orchestration","tasks","run","Bash","model","node","read","orchestrator","files","desktop app","agent-orchestration",{"skillNo":121,"name":122,"nameZh":6,"description":123,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":124,"categories":125,"keywords":126,"publishedAt":43},"01M6Q0ETK41VGNMYEXW0T00AJN","youtube-transcript","Fetch YouTube transcripts through DeepAPI or local fallback tooling and save clean text output.","https:\u002F\u002Fgithub.com\u002Fduc-duy-1612\u002FOnline-Delivery-Systems-Web\u002Fblob\u002Fmain\u002F.agents\u002Fskills_archive\u002Fyoutube-transcript\u002FSKILL.md",[],[127,128,129,130,131,132,133,134,135,136,137,138,139,36,35,140,141,142,143,144],"transcript","youtube transcript","youtube","safe","github","codex","research","license","https","text","fetch","tooling","clean","local","output","deepapi","youtube transcripts","blob",{"skillNo":146,"name":147,"nameZh":6,"description":148,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":149,"categories":150,"keywords":162,"publishedAt":43},"01M6Q0EPPH2PVMDW40017F8WX0","youtube-transcript-skills","Fetch YouTube video transcripts, search videos\u002Fchannels, browse channels, and extract playlists via the getyoutubetranscript.com API - free tier, no card required.","https:\u002F\u002Fgithub.com\u002Fduc-duy-1612\u002FOnline-Delivery-Systems-Web\u002Fblob\u002Fmain\u002F.agents\u002Fskills_archive\u002Fyoutube-transcript-skills\u002FSKILL.md",[151,154,157],{"categoryNo":152,"categoryName":153,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000010","后端与接口",{"categoryNo":155,"categoryName":156,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000016","图像与多媒体",{"categoryNo":158,"categoryName":159,"taxonomyType":160,"taxonomyTypeDesc":161},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[127,128,129,163,130,164,131,132,134,165,166,167,168,137,169,36,35,170,171,172],"youtube transcript skills","gemini","search","api","Video","browse","api-integration","free","card","channels",{"skillNo":174,"name":175,"nameZh":6,"description":176,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":177,"categories":178,"keywords":183,"publishedAt":43},"01M6Q0EAB96PX83W5PTE8A0R8D","youtube-notetaker","Turn YouTube talks into local study notes with slides, transcripts, editable annotations, and a markdown-backed viewer.","https:\u002F\u002Fgithub.com\u002Fduc-duy-1612\u002FOnline-Delivery-Systems-Web\u002Fblob\u002Fmain\u002F.agents\u002Fskills_archive\u002Fyoutube-notetaker\u002FSKILL.md",[179,182],{"categoryNo":180,"categoryName":181,"taxonomyType":20,"taxonomyTypeDesc":21},"01JBSEEDCAT000000000000003","演示文稿",{"categoryNo":155,"categoryName":156,"taxonomyType":20,"taxonomyTypeDesc":21},[129,184,185,108,130,186,187,188,189,134,167,190,191,192,193,35,194,140,195,196],"youtube notetaker","notetaker","workflow","official","notes","code","markdown","slides","annotations","README","study","dair-academy","viewer",{"skillNo":198,"name":199,"nameZh":6,"description":200,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":6,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":201,"categories":202,"keywords":204,"publishedAt":43},"01M6Q0E55F0GW5MX48V8HXS8B5","youtube-full","Fetch YouTube transcripts, search videos, browse channels, and extract playlists via TranscriptAPI — no yt-dlp, no Google API key, works from any cloud server.","https:\u002F\u002Fgithub.com\u002Fduc-duy-1612\u002FOnline-Delivery-Systems-Web\u002Fblob\u002Fmain\u002F.agents\u002Fskills_archive\u002Fyoutube-full\u002FSKILL.md",[203],{"categoryNo":152,"categoryName":153,"taxonomyType":20,"taxonomyTypeDesc":21},[205,129,206,207,130,164,165,208,209,166,167,168,137,210,169,36,35,170,211,143],"youtube full","full","google","server","dlp","credits","automatic"]