Summary
Optimistic UI applies a mutation to local state immediately, then confirms or rolls back once the server responds. The pattern eliminates the visible delay between user action and UI change. TanStack Query's `useMutation` lifecycle and React 19's `useOptimistic` are the two main implementation paths.
Jump to the interview angleOptimistic UI updates the client as if a mutation already succeeded, then rolls back if the server errors.
The payoff is instant perceived response: a like flips immediately, a todo appears the moment you press Enter. Reverting on error is rare enough that users accept it.
Two concerns compound the pattern:
- Idempotency: if a retry sends the request twice, the server must produce the same result. Use idempotency keys or PUT/PATCH, not bare POST.
- Race conditions: a refetch can resolve after your optimistic write and overwrite it. Cancel in-flight queries in
onMutatebefore writing.
TanStack Query v5 — snapshot, optimistic write, rollback
Three callbacks form the full lifecycle: cancel in-flight queries in onMutate, snapshot and write optimistically, then roll back in onError and sync in onSettled.
import { useMutation, useQueryClient } from "@tanstack/react-query";
type Todo = { id: string; text: string; done: boolean };
function useTodoToggle() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) =>
fetch(`/api/todos/${id}/toggle`, { method: "PATCH" }).then((r) =>
r.json()
),
onMutate: async (id) => {
// 1. Cancel outgoing refetches — prevents stale data overwriting optimistic value
await queryClient.cancelQueries({ queryKey: ["todos"] });
// 2. Snapshot current cache
const previous = queryClient.getQueryData<Todo[]>(["todos"]);
// 3. Write optimistic update
queryClient.setQueryData<Todo[]>(["todos"], (old = []) =>
old.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
);
return { previous }; // context passed to onError / onSettled
},
onError: (_err, _id, context) => {
// Restore snapshot on failure
if (context?.previous) {
queryClient.setQueryData(["todos"], context.previous);
}
},
onSettled: () => {
// Sync cache with server truth regardless of outcome
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
});
}cancelQueries stops a race where an in-flight refetch overwrites the optimistic value. The snapshot returned from onMutate lets onError restore previous state.
React 19 useOptimistic needs no manual rollback
Call useOptimistic(serverValue, reducer) to get [optimisticState, setOptimistic]. Inside startTransition, call setOptimistic(nextValue) before the await. React displays the optimistic value until the transition settles or throws, then reverts automatically — no setQueryData needed.
Interview angle
Interviewers test whether you know the three-step lifecycle and where it breaks. Explain onMutate (snapshot + cancel + write), onError (rollback from context), and onSettled (invalidate). Mention race conditions and idempotency as the two API-layer concerns the UI pattern cannot solve alone.
Soundbite: "Write optimistically in onMutate, roll back in onError with the snapshot, and always invalidate in onSettled."
Key terms
- onMutate
- TanStack Query callback that runs before the fetch; returns a context snapshot for rollback.
- onError
- Mutation callback receiving the `onMutate` context; restores the cache snapshot on failure.
- onSettled
- Fires after success or error; the right place to call `invalidateQueries`.
- useOptimistic
- React 19 hook that applies an optimistic reducer and auto-reverts when the transition settles.
- idempotency
- Property where repeating a request produces the same result; required for safe retries.