Skip to content
fearchitect
State & Data

Optimistic UI & Mutations

Update the UI before the server replies; roll back on error.

By Abas TurabliReviewed

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 angle

Optimistic 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 onMutate before writing.
Optimistic update applies immediately to the cache; rollback restores the snapshot on error, and invalidation syncs on success.

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.

TanStack Query v5 — snapshot, optimistic write, rollbacktsx
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.

Further reading

Search fearchitect

Jump to a topic, mode, or action.