open atlas
↑ Back to track
Queues, Streams, Eventing QUE · 07 · 09

Eventual-consistency UX: code reading

Read real React/TanStack Query snippets over an async backend, predict the UX bug, and pick the highest-leverage fix a senior would make first.

QUE Senior ◷ 14 min
Level
FoundationsJuniorMiddleSenior

Eventual-consistency bugs hide in the mutation handler and the retry path. Read the code, predict what the user sees when the consumer lags or the request fails, then choose the fix a senior engineer would make first.

Goal

Practise the loop you run in every async-UX review: read the mutation, predict the divergence between optimistic state and server truth, and reach for the contract-completing fix — snapshot, rollback, reconcile, key — before adding spinners.

Snippet 1 — the half-finished optimistic update

const mutation = useMutation({
  mutationFn: (todo) => api.addTodo(todo),
  onMutate: async (todo) => {
    await qc.cancelQueries({ queryKey: ['todos'] });
    qc.setQueryData(['todos'], (old) => [...old, todo]);
    // returns nothing
  },
  onSettled: () => qc.invalidateQueries({ queryKey: ['todos'] }),
});
Quiz

The request occasionally fails (the consumer rejects the write). What does the user see, and what is the single highest-leverage fix?

Snippet 2 — the retry button

function PayButton({ orderId, amount }) {
  const [status, setStatus] = useState('idle');
  async function pay() {
    setStatus('pending');
    await fetch('/api/charge', {
      method: 'POST',
      body: JSON.stringify({ orderId, amount }),
    });
    setStatus('done');
  }
  // user sees no progress, clicks pay() again on a slow network
  return <button onClick={pay}>Pay</button>;
}
Quiz

On a slow network the user clicks twice. What goes wrong, and what is the right fix?

Snippet 3 — the fake-success toast

async function publish(post) {
  const res = await fetch('/api/posts', {
    method: 'POST',
    body: JSON.stringify(post),
  });
  if (res.status === 202) {
    toast.success('Published!');
    await qc.invalidateQueries({ queryKey: ['posts'] });
  }
}
Quiz

The POST returns 202 and a consumer writes the post ~800ms later. What two things are wrong here?

Snippet 4 — the conflict that overwrites

function onIncomingUpdate(remote) {
  // local has unsaved edits; remote arrived from another client
  setDoc(remote); // last write wins, silently
}
Quiz

Two clients edit the same document body. This handler runs on the incoming remote update. What is the senior objection, and the better shape?

Recap

Every async-UX bug is read in the mutation and the retry path: an optimistic update without a snapshot-and-onError rollback strands a failed write on screen; a POST without an idempotency key turns a double-click into a double charge; collapsing 202 into a success toast plus an immediate refetch is fake success racing the consumer; and a silent last-write-wins handler destroys a user’s unsaved edits. Complete the apply-send-reconcile contract, key the retries, show honest pending states, and reconcile conflicts deliberately instead of overwriting. Now when you open a mutation handler in a PR, your first three checks are: is there a snapshot, is there an onError rollback, and does every retry path carry an idempotency key?

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.