Back to docs

Recipe: Optimistic mutation with rollback

Update UI instantly, then reconcile with the server. If the mutation fails, roll back to the previous state.

The pattern

When a user performs a mutation (like toggling a todo or updating a profile), apply the change to local state immediately. Fire the API call in the background. If the server returns an error, revert the state and surface the failure.

Step 1 — Snapshot before mutation

const prev = [...items];
setItems(updated);

try {
  await api.update(id, payload);
} catch {
  setItems(prev);
  toast.error("Failed to update");
}

Step 2 — Handle concurrent mutations

Use a ref to track pending mutations. If the user triggers another mutation before the first resolves, either queue it or abort the in-flight request with an AbortController.

Step 3 — Server authority

The server is always the source of truth. After a successful mutation, re-fetch the affected resource to ensure the client state matches what the database actually holds. This catches edge cases like duplicate submissions or field-level validation failures.

Meridian tip: Combine this with a stale-while-revalidate cache strategy for reads. Optimistic writes feel instant; SWR keeps reads fresh without blocking navigation.