HighX
Client

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.

On this page