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
Use qc.set() to write the optimistic value before calling mutate(), then call qc.invalidate() in the catch block to restore server state on failure.
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.