Optimistic updates
Showing the effect of a write before the server has agreed to it.
useMutation sends straight through and the UI updates when the patch arrives —
usually fast enough. When you want it to move first, describe the effect:
const create = useMutation(api.notes.create);
await create({ id, title: "Untitled" }, {
optimistic: {
key: queryKey(api.notes.list, {}),
apply: (rows) => [...rows, { id, title: "Untitled", updatedAt: Date.now() }],
},
});key names the cached query to change; apply receives its current rows and
returns the rows to show.
Rollback is automatic
The change is reverted if the server rejects the mutation — and by the same code if the client closes with the mutation still unacknowledged. There is no rollback of your own to write.
Choose the id on the client for inserts
The cache merges rows by id. A drawn row and the stored row that differ only by id are two rows, so the note appears twice until a reload. Passing the id also makes the write idempotent in storage, which matters when the offline queue replays it.
Without React
Through the client directly:
await client.mutate(api.notes.edit, { id, title }, {
optimisticKey: queryKey(api.notes.list, {}),
optimistic: (rows) => rows.map((row) => (row["id"] === id ? { ...row, title } : row)),
});When not to bother
If a mutation's effect reaches the UI through a subscription anyway, an optimistic update buys one round trip and costs a second description of the same change that can drift from the handler. Reach for it on the interactions people feel — typing, toggling, dragging — and leave the rest alone.