SKILL.md
SKILL.mdBrowse 2 files
2,769 tokens
11,850 bytes
Token encoding: o200k_base
Snapshot 09776a8
1---2name: db-core/mutations-optimistic3description: >4 collection.insert, collection.update (Immer-style draft proxy),5 collection.delete. createOptimisticAction (onMutate + mutationFn).6 createPacedMutations with debounceStrategy, throttleStrategy, queueStrategy.7 createTransaction, getActiveTransaction, ambient transaction context.8 Transaction lifecycle (pending/persisting/completed/failed). Mutation merging.9 onInsert/onUpdate/onDelete handlers. PendingMutation type. Transaction.isPersisted.10type: sub-skill11library: db12library_version: '0.6.17'13sources:14 - 'TanStack/db:docs/guides/mutations.md'15 - 'TanStack/db:packages/db/src/transactions.ts'16 - 'TanStack/db:packages/db/src/optimistic-action.ts'17 - 'TanStack/db:packages/db/src/paced-mutations.ts'18---19 20# Mutations & Optimistic State21 22> **Depends on:** `db-core/collection-setup` -- you need a configured collection23> (with `getKey`, sync adapter, and optionally `onInsert`/`onUpdate`/`onDelete`24> handlers) before you can mutate.25 26TanStack DB mutations follow a unidirectional loop:27**optimistic mutation -> handler persists -> handler waits for sync/ack -> confirmed state**.28Optimistic state is applied in the current tick and dropped when the handler resolves.29 30---31 32## Setup -- Collection Write Operations33 34### insert35 36```ts37import { safeRandomUUID } from '@tanstack/db'38 39// Single item40todoCollection.insert({41 id: safeRandomUUID(),42 text: 'Buy groceries',43 completed: false,44})45 46// Multiple items47todoCollection.insert([48 { id: safeRandomUUID(), text: 'Buy groceries', completed: false },49 { id: safeRandomUUID(), text: 'Walk dog', completed: false },50])51 52// With metadata / non-optimistic53todoCollection.insert(item, { metadata: { source: 'import' } })54todoCollection.insert(item, { optimistic: false })55```56 57### update (Immer-style draft proxy)58 59```ts60// Single item -- mutate the draft, do NOT reassign it61todoCollection.update(todo.id, (draft) => {62 draft.completed = true63 draft.completedAt = new Date()64})65 66// Multiple items67todoCollection.update([id1, id2], (drafts) => {68 drafts.forEach((d) => {69 d.completed = true70 })71})72 73// With metadata74todoCollection.update(75 todo.id,76 { metadata: { reason: 'user-edit' } },77 (draft) => {78 draft.text = 'Updated'79 },80)81```82 83### delete84 85```ts86todoCollection.delete(todo.id)87todoCollection.delete([id1, id2])88todoCollection.delete(todo.id, { metadata: { reason: 'completed' } })89```90 91All three return a `Transaction` object. Use `tx.isPersisted.promise` to await92settlement or catch rollback errors. For a non-empty transaction, this normally93means its `mutationFn` returned; it proves upload, confirmation, or read-back94only when that function waits for the backend observation before returning.95 96Do not start or await collection preloads, live-query preloads, or direct97`loadSubset()` calls inside `mutationFn`. Sync commits queue behind mutation98persistence, so the preload can wait on the mutation that is waiting on it.99Use the collection adapter's documented mutation acknowledgement pattern.100 101---102 103## Core Patterns104 105### 1. createOptimisticAction -- intent-based mutations106 107Use when the optimistic change is a _guess_ at how the server will transform108the data, or when you need to mutate multiple collections atomically.109 110```ts111import { createOptimisticAction } from '@tanstack/db'112 113const likePost = createOptimisticAction<string>({114 // MUST be synchronous -- applied in the current tick115 onMutate: (postId) => {116 postCollection.update(postId, (draft) => {117 draft.likeCount += 1118 draft.likedByMe = true119 })120 },121 mutationFn: async (postId, { transaction }) => {122 await api.posts.like(postId)123 // IMPORTANT: wait for server state to sync back before returning124 await postCollection.utils.refetch()125 },126})127 128// Returns a Transaction129const tx = likePost(postId)130await tx.isPersisted.promise131```132 133Multi-collection example:134 135```ts136const createProject = createOptimisticAction<{ name: string; ownerId: string }>(137 {138 onMutate: ({ name, ownerId }) => {139 projectCollection.insert({ id: safeRandomUUID(), name, ownerId })140 userCollection.update(ownerId, (d) => {141 d.projectCount += 1142 })143 },144 mutationFn: async ({ name, ownerId }) => {145 await api.projects.create({ name, ownerId })146 await Promise.all([147 projectCollection.utils.refetch(),148 userCollection.utils.refetch(),149 ])150 },151 },152)153```154 155### 2. createPacedMutations -- auto-save with debounce / throttle / queue156 157```ts158import { createPacedMutations, debounceStrategy } from '@tanstack/db'159 160const autoSaveNote = createPacedMutations<string>({161 onMutate: (text) => {162 noteCollection.update(noteId, (draft) => {163 draft.body = text164 })165 },166 mutationFn: async ({ transaction }) => {167 const mutation = transaction.mutations[0]168 await api.notes.update(mutation.key, mutation.changes)169 await noteCollection.utils.refetch()170 },171 strategy: debounceStrategy({ wait: 500 }),172})173 174// Each call resets the debounce timer; mutations merge into one transaction175autoSaveNote('Hello')176autoSaveNote('Hello, world') // only this version persists177```178 179Other strategies:180 181```ts182import { throttleStrategy, queueStrategy } from '@tanstack/db'183 184// Evenly spaced (sliders, scroll)185throttleStrategy({ wait: 200, leading: true, trailing: true })186 187// Sequential FIFO -- every mutation persisted in order188queueStrategy({ wait: 0, maxSize: 100 })189```190 191### 3. createTransaction -- manual batching192 193```ts194import { createTransaction } from '@tanstack/db'195 196const tx = createTransaction({197 autoCommit: false, // wait for explicit commit()198 mutationFn: async ({ transaction }) => {199 await api.batchUpdate(transaction.mutations)200 },201})202 203tx.mutate(() => {204 todoCollection.update(id1, (d) => {205 d.status = 'reviewed'206 })207 todoCollection.update(id2, (d) => {208 d.status = 'reviewed'209 })210})211 212// User reviews... then commits or rolls back213await tx.commit()214// OR: tx.rollback()215```216 217Inside `tx.mutate(() => { ... })`, the transaction is pushed onto an ambient218stack. Any `collection.insert/update/delete` call joins the ambient transaction219automatically via `getActiveTransaction()`. That scope is synchronous:220collection operations after an `await` do not join it. Put async work in221`mutationFn`, or call `mutate()` again before committing.222 223For mutations captured by a manual transaction, collection-level224`onInsert`/`onUpdate`/`onDelete` handlers are not invoked automatically. The225manual transaction's `mutationFn` is responsible for persisting226`transaction.mutations`. This makes `createTransaction({ autoCommit: false })`227a good fit for draft-style flows where local state updates immediately but the228server call waits for Save/Blur; call `tx.rollback()` to discard the optimistic229changes.230 231### 4. Mutation handlers with automatic refetch (QueryCollection pattern)232 233```ts234const todoCollection = createCollection(235 queryCollectionOptions({236 queryKey: ['todos'],237 queryFn: () => api.todos.getAll(),238 getKey: (t) => t.id,239 onInsert: async ({ transaction }) => {240 await Promise.all(241 transaction.mutations.map((m) => api.todos.create(m.modified)),242 )243 // Query Collection refetches after the handler completes and awaits it.244 },245 onUpdate: async ({ transaction }) => {246 await Promise.all(247 transaction.mutations.map((m) =>248 api.todos.update(m.original.id, m.changes),249 ),250 )251 },252 onDelete: async ({ transaction }) => {253 await Promise.all(254 transaction.mutations.map((m) => api.todos.delete(m.original.id)),255 )256 },257 }),258)259```260 261For ElectricCollection, return `{ txid }` instead of refetching:262 263```ts264onUpdate: async ({ transaction }) => {265 const txids = await Promise.all(266 transaction.mutations.map(async (m) => {267 const res = await api.todos.update(m.original.id, m.changes)268 return res.txid269 }),270 )271 return { txid: txids }272}273```274 275---276 277## Common Mistakes278 279### CRITICAL: Passing an object to update() instead of a draft callback280 281```ts282// WRONG -- silently fails or throws283collection.update(id, { ...item, title: 'new' })284 285// CORRECT -- mutate the draft proxy286collection.update(id, (draft) => {287 draft.title = 'new'288})289```290 291### CRITICAL: Hallucinating mutation API signatures292 293The most common AI-generated errors:294 295- Inventing handler signatures (e.g. `onMutate` on a collection config)296- Confusing `createOptimisticAction` with `createTransaction`297- Wrong PendingMutation property names (`mutation.data` does not exist --298 use `mutation.modified`, `mutation.changes`, `mutation.original`)299- Missing the ambient transaction pattern300 301Always reference the exact types in `references/transaction-api.md`.302 303### CRITICAL: onMutate returning a Promise304 305`onMutate` in `createOptimisticAction` **must be synchronous**. Optimistic state306is applied in the current tick. Returning a Promise throws307`OnMutateMustBeSynchronousError`.308 309```ts310// WRONG311createOptimisticAction({312 onMutate: async (text) => {313 collection.insert({ id: await generateId(), text })314 },315 ...316})317 318// CORRECT319createOptimisticAction({320 onMutate: (text) => {321 collection.insert({ id: safeRandomUUID(), text })322 },323 ...324})325```326 327### CRITICAL: Mutations without handler or ambient transaction328 329Collection mutations require either:330 3311. An `onInsert`/`onUpdate`/`onDelete` handler on the collection, OR3322. An ambient transaction from `createTransaction`/`createOptimisticAction`333 334Without either, throws `MissingInsertHandlerError` (or the Update/Delete variant).335 336### HIGH: Calling .mutate() after transaction is no longer pending337 338Transactions only accept new mutations while in `pending` state. Calling339`mutate()` after `commit()` or `rollback()` throws340`TransactionNotPendingMutateError`. Create a new transaction instead.341 342### HIGH: Changing primary key via update343 344The update proxy detects key changes and throws `KeyUpdateNotAllowedError`.345Primary keys are immutable once set. If you need a different key, delete and346re-insert.347 348### HIGH: Inserting item with duplicate key349 350If an item with the same key already exists (synced or optimistic), throws351`DuplicateKeyError`. Always generate a unique key (e.g. `safeRandomUUID()`)352or check before inserting.353 354### HIGH: Manually refetching inside a Query Collection handler355 356Query Collection automatically refetches after `onInsert`, `onUpdate`, and357`onDelete` complete, and waits for that refetch before the mutation finishes.358Calling `utils.refetch()` inside the handler sends a redundant request.359 360```ts361// WRONG -- causes one manual and one automatic refetch362onInsert: async ({ transaction }) => {363 await api.createTodo(transaction.mutations[0].modified)364 await collection.utils.refetch()365}366 367// CORRECT -- automatic refetch is awaited after this returns368onInsert: async ({ transaction }) => {369 await api.createTodo(transaction.mutations[0].modified)370}371```372 373When the handler writes the confirmed server result with direct-write utilities,374return `{ refetch: false }` to skip the automatic refetch.375 376---377 378## Tension: Optimistic Speed vs. Data Consistency379 380Instant optimistic updates create a window where client state diverges from381server state. If the handler fails, the rollback removes the optimistic state --382which can discard user work the user thought was saved. Consider:383 384- Showing pending/saving indicators so users know state is unconfirmed385- Using `{ optimistic: false }` for destructive operations386- Designing idempotent server endpoints so retries are safe387- Handling `tx.isPersisted.promise` rejection to surface errors to the user388 389---390 391## References392 393- [Transaction API Reference](references/transaction-api.md) -- createTransaction config,394 Transaction object, PendingMutation type, mutation merging rules, strategy types395- [TanStack DB Mutations Guide](https://tanstack.com/db/latest/docs/guides/mutations)396 Discovery context
Discovered by repository scan. No exact path reference found in the snapshot’s root AGENTS.md.