db-core/mutations-optimistic

collection.insert, collection.update (Immer-style draft proxy), collection.delete. createOptimisticAction (onMutate + mutationFn). createPacedMutations with debounceStrategy, throttleStrategy, queueStrategy. createTransaction, getActiveTransaction, ambient transaction context. Transaction lifecycle (pending/persisting/completed/failed). Mutation merging. onInsert/onUpdate/onDelete handlers. PendingMutation type. Transaction.isPersisted.

Install
npx skills add 'https://github.com/TanStack/db/tree/main/packages/db/skills/db-core/mutations-optimistic'
Download bundle ↓
main · 09776a8Scanned 2026-09-17

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

SKILL.md

SKILL.mdBrowse 2 files
View on GitHub
---name: db-core/mutations-optimisticdescription: >  collection.insert, collection.update (Immer-style draft proxy),  collection.delete. createOptimisticAction (onMutate + mutationFn).  createPacedMutations with debounceStrategy, throttleStrategy, queueStrategy.  createTransaction, getActiveTransaction, ambient transaction context.  Transaction lifecycle (pending/persisting/completed/failed). Mutation merging.  onInsert/onUpdate/onDelete handlers. PendingMutation type. Transaction.isPersisted.type: sub-skilllibrary: dblibrary_version: '0.6.17'sources:  - 'TanStack/db:docs/guides/mutations.md'  - 'TanStack/db:packages/db/src/transactions.ts'  - 'TanStack/db:packages/db/src/optimistic-action.ts'  - 'TanStack/db:packages/db/src/paced-mutations.ts'--- # Mutations & Optimistic State > **Depends on:** `db-core/collection-setup` -- you need a configured collection> (with `getKey`, sync adapter, and optionally `onInsert`/`onUpdate`/`onDelete`> handlers) before you can mutate. TanStack DB mutations follow a unidirectional loop:**optimistic mutation -> handler persists -> handler waits for sync/ack -> confirmed state**.Optimistic state is applied in the current tick and dropped when the handler resolves. --- ## Setup -- Collection Write Operations ### insert ```tsimport { safeRandomUUID } from '@tanstack/db' // Single itemtodoCollection.insert({  id: safeRandomUUID(),  text: 'Buy groceries',  completed: false,}) // Multiple itemstodoCollection.insert([  { id: safeRandomUUID(), text: 'Buy groceries', completed: false },  { id: safeRandomUUID(), text: 'Walk dog', completed: false },]) // With metadata / non-optimistictodoCollection.insert(item, { metadata: { source: 'import' } })todoCollection.insert(item, { optimistic: false })``` ### update (Immer-style draft proxy) ```ts// Single item -- mutate the draft, do NOT reassign ittodoCollection.update(todo.id, (draft) => {  draft.completed = true  draft.completedAt = new Date()}) // Multiple itemstodoCollection.update([id1, id2], (drafts) => {  drafts.forEach((d) => {    d.completed = true  })}) // With metadatatodoCollection.update(  todo.id,  { metadata: { reason: 'user-edit' } },  (draft) => {    draft.text = 'Updated'  },)``` ### delete ```tstodoCollection.delete(todo.id)todoCollection.delete([id1, id2])todoCollection.delete(todo.id, { metadata: { reason: 'completed' } })``` All three return a `Transaction` object. Use `tx.isPersisted.promise` to awaitsettlement or catch rollback errors. For a non-empty transaction, this normallymeans its `mutationFn` returned; it proves upload, confirmation, or read-backonly when that function waits for the backend observation before returning. Do not start or await collection preloads, live-query preloads, or direct`loadSubset()` calls inside `mutationFn`. Sync commits queue behind mutationpersistence, so the preload can wait on the mutation that is waiting on it.Use the collection adapter's documented mutation acknowledgement pattern. --- ## Core Patterns ### 1. createOptimisticAction -- intent-based mutations Use when the optimistic change is a _guess_ at how the server will transformthe data, or when you need to mutate multiple collections atomically. ```tsimport { createOptimisticAction } from '@tanstack/db' const likePost = createOptimisticAction<string>({  // MUST be synchronous -- applied in the current tick  onMutate: (postId) => {    postCollection.update(postId, (draft) => {      draft.likeCount += 1      draft.likedByMe = true    })  },  mutationFn: async (postId, { transaction }) => {    await api.posts.like(postId)    // IMPORTANT: wait for server state to sync back before returning    await postCollection.utils.refetch()  },}) // Returns a Transactionconst tx = likePost(postId)await tx.isPersisted.promise``` Multi-collection example: ```tsconst createProject = createOptimisticAction<{ name: string; ownerId: string }>(  {    onMutate: ({ name, ownerId }) => {      projectCollection.insert({ id: safeRandomUUID(), name, ownerId })      userCollection.update(ownerId, (d) => {        d.projectCount += 1      })    },    mutationFn: async ({ name, ownerId }) => {      await api.projects.create({ name, ownerId })      await Promise.all([        projectCollection.utils.refetch(),        userCollection.utils.refetch(),      ])    },  },)``` ### 2. createPacedMutations -- auto-save with debounce / throttle / queue ```tsimport { createPacedMutations, debounceStrategy } from '@tanstack/db' const autoSaveNote = createPacedMutations<string>({  onMutate: (text) => {    noteCollection.update(noteId, (draft) => {      draft.body = text    })  },  mutationFn: async ({ transaction }) => {    const mutation = transaction.mutations[0]    await api.notes.update(mutation.key, mutation.changes)    await noteCollection.utils.refetch()  },  strategy: debounceStrategy({ wait: 500 }),}) // Each call resets the debounce timer; mutations merge into one transactionautoSaveNote('Hello')autoSaveNote('Hello, world') // only this version persists``` Other strategies: ```tsimport { throttleStrategy, queueStrategy } from '@tanstack/db' // Evenly spaced (sliders, scroll)throttleStrategy({ wait: 200, leading: true, trailing: true }) // Sequential FIFO -- every mutation persisted in orderqueueStrategy({ wait: 0, maxSize: 100 })``` ### 3. createTransaction -- manual batching ```tsimport { createTransaction } from '@tanstack/db' const tx = createTransaction({  autoCommit: false, // wait for explicit commit()  mutationFn: async ({ transaction }) => {    await api.batchUpdate(transaction.mutations)  },}) tx.mutate(() => {  todoCollection.update(id1, (d) => {    d.status = 'reviewed'  })  todoCollection.update(id2, (d) => {    d.status = 'reviewed'  })}) // User reviews... then commits or rolls backawait tx.commit()// OR: tx.rollback()``` Inside `tx.mutate(() => { ... })`, the transaction is pushed onto an ambientstack. Any `collection.insert/update/delete` call joins the ambient transactionautomatically via `getActiveTransaction()`. That scope is synchronous:collection operations after an `await` do not join it. Put async work in`mutationFn`, or call `mutate()` again before committing. For mutations captured by a manual transaction, collection-level`onInsert`/`onUpdate`/`onDelete` handlers are not invoked automatically. Themanual transaction's `mutationFn` is responsible for persisting`transaction.mutations`. This makes `createTransaction({ autoCommit: false })`a good fit for draft-style flows where local state updates immediately but theserver call waits for Save/Blur; call `tx.rollback()` to discard the optimisticchanges. ### 4. Mutation handlers with automatic refetch (QueryCollection pattern) ```tsconst todoCollection = createCollection(  queryCollectionOptions({    queryKey: ['todos'],    queryFn: () => api.todos.getAll(),    getKey: (t) => t.id,    onInsert: async ({ transaction }) => {      await Promise.all(        transaction.mutations.map((m) => api.todos.create(m.modified)),      )      // Query Collection refetches after the handler completes and awaits it.    },    onUpdate: async ({ transaction }) => {      await Promise.all(        transaction.mutations.map((m) =>          api.todos.update(m.original.id, m.changes),        ),      )    },    onDelete: async ({ transaction }) => {      await Promise.all(        transaction.mutations.map((m) => api.todos.delete(m.original.id)),      )    },  }),)``` For ElectricCollection, return `{ txid }` instead of refetching: ```tsonUpdate: async ({ transaction }) => {  const txids = await Promise.all(    transaction.mutations.map(async (m) => {      const res = await api.todos.update(m.original.id, m.changes)      return res.txid    }),  )  return { txid: txids }}``` --- ## Common Mistakes ### CRITICAL: Passing an object to update() instead of a draft callback ```ts// WRONG -- silently fails or throwscollection.update(id, { ...item, title: 'new' }) // CORRECT -- mutate the draft proxycollection.update(id, (draft) => {  draft.title = 'new'})``` ### CRITICAL: Hallucinating mutation API signatures The most common AI-generated errors: - Inventing handler signatures (e.g. `onMutate` on a collection config)- Confusing `createOptimisticAction` with `createTransaction`- Wrong PendingMutation property names (`mutation.data` does not exist --  use `mutation.modified`, `mutation.changes`, `mutation.original`)- Missing the ambient transaction pattern Always reference the exact types in `references/transaction-api.md`. ### CRITICAL: onMutate returning a Promise `onMutate` in `createOptimisticAction` **must be synchronous**. Optimistic stateis applied in the current tick. Returning a Promise throws`OnMutateMustBeSynchronousError`. ```ts// WRONGcreateOptimisticAction({  onMutate: async (text) => {    collection.insert({ id: await generateId(), text })  },  ...}) // CORRECTcreateOptimisticAction({  onMutate: (text) => {    collection.insert({ id: safeRandomUUID(), text })  },  ...})``` ### CRITICAL: Mutations without handler or ambient transaction Collection mutations require either: 1. An `onInsert`/`onUpdate`/`onDelete` handler on the collection, OR2. An ambient transaction from `createTransaction`/`createOptimisticAction` Without either, throws `MissingInsertHandlerError` (or the Update/Delete variant). ### HIGH: Calling .mutate() after transaction is no longer pending Transactions only accept new mutations while in `pending` state. Calling`mutate()` after `commit()` or `rollback()` throws`TransactionNotPendingMutateError`. Create a new transaction instead. ### HIGH: Changing primary key via update The update proxy detects key changes and throws `KeyUpdateNotAllowedError`.Primary keys are immutable once set. If you need a different key, delete andre-insert. ### HIGH: Inserting item with duplicate key If an item with the same key already exists (synced or optimistic), throws`DuplicateKeyError`. Always generate a unique key (e.g. `safeRandomUUID()`)or check before inserting. ### HIGH: Manually refetching inside a Query Collection handler Query Collection automatically refetches after `onInsert`, `onUpdate`, and`onDelete` complete, and waits for that refetch before the mutation finishes.Calling `utils.refetch()` inside the handler sends a redundant request. ```ts// WRONG -- causes one manual and one automatic refetchonInsert: async ({ transaction }) => {  await api.createTodo(transaction.mutations[0].modified)  await collection.utils.refetch()} // CORRECT -- automatic refetch is awaited after this returnsonInsert: async ({ transaction }) => {  await api.createTodo(transaction.mutations[0].modified)}``` When the handler writes the confirmed server result with direct-write utilities,return `{ refetch: false }` to skip the automatic refetch. --- ## Tension: Optimistic Speed vs. Data Consistency Instant optimistic updates create a window where client state diverges fromserver state. If the handler fails, the rollback removes the optimistic state --which can discard user work the user thought was saved. Consider: - Showing pending/saving indicators so users know state is unconfirmed- Using `{ optimistic: false }` for destructive operations- Designing idempotent server endpoints so retries are safe- Handling `tx.isPersisted.promise` rejection to surface errors to the user --- ## References - [Transaction API Reference](references/transaction-api.md) -- createTransaction config,  Transaction object, PendingMutation type, mutation merging rules, strategy types- [TanStack DB Mutations Guide](https://tanstack.com/db/latest/docs/guides/mutations) 
Discovery context

Discovered by repository scan. No exact path reference found in the snapshot’s root AGENTS.md.