Skip to content

Optimistic Updates

Problem

To feel instant, the UI should reflect a mutation's expected result immediately — before the server confirms it. If the server rejects the change, the UI must roll back to the previous state.

Solution

ts
const userId = 1;
const key = ['users', userId];
const patch = { name: 'Updated Name' };

const updateUser = createMutation((input: Partial<User>, signal: AbortSignal) =>
  api.put<User>('/users/{id}', { params: { id: userId }, body: input, signal }),
);

// Apply optimistic update immediately
qc.set<User>(key, (old) => ({ ...old!, ...patch }));

try {
  await updateUser.mutate(patch);
  // Server confirmed — force sync
  qc.invalidate(key);
} catch {
  // Server rejected — roll back
  qc.invalidate(key);
}

// Optional: cancel an in-flight mutation directly
// updateUser.cancel();

Pitfalls

  • After applying an optimistic update, the UI shows stale data until the server confirms. Always set a pending/loading indicator so the user knows a mutation is in flight.
  • If the rollback function closes over stale state captured before the optimistic write, nested updates can produce an incorrect rollback target. Capture the previous value immediately before mutating.
  • Concurrent mutations on the same resource each apply and roll back independently. The rollback order may not match the mutation order. Use a sequential mutation queue for the same resource key.