CodePark/ questions / State & data-fetching architecture
START PRACTICINGPRACTICE
Frontend question · Frontend system design › State & data-fetching architecture

How would you implement optimistic updates for a "like" button in a feed, including rollback when the request fails?

useMutation({
  mutationFn: likePost,
  onMutate: async (id) => {
    await qc.cancelQueries({ queryKey: ["feed"] });
    const prev = qc.getQueryData(["feed"]);
    qc.setQueryData(["feed"], (f) => toggleLike(f, id));
    return { prev };
  },
  onError: (_e, _id, ctx) => qc.setQueryData(["feed"], ctx.prev),
  onSettled: () => qc.invalidateQueries({ queryKey: ["feed"] }),
});

When the user clicks, update the cache immediately and send the request in the background. The count and icon change with no wait. This is an optimistic update: the UI assumes the request will succeed.

The steps:

  • Cancel in-flight fetches for that data, so they cannot overwrite the optimistic value.
  • Take a snapshot of the previous value.
  • Apply the change to the cache.
  • Keep the snapshot in the mutation context.
  • On error, restore the snapshot and show a non-blocking toast (a small message that does not stop the user).
  • On settle (after success or error), invalidate the data so the server's real count wins.

Edge cases matter. Fast double clicks should not send conflicting requests. So either debounce the clicks or make the API idempotent. Idempotent means sending the same request twice has the same effect as sending it once. For example, use PUT /posts/1/like and DELETE instead of a toggle.

The same post may appear in several caches, such as the feed, the profile and the detail page. A normalised store, or updating each cache key, keeps them consistent.

Optimistic updates fit actions that almost always succeed. For payments or actions you cannot undo, show a pending state instead. In React, useOptimistic covers the same idea for form actions.

  • Cancel in-flight queries, take a snapshot, then apply the change.
  • On error, roll back to the snapshot and tell the user.
  • When the request settles, invalidate or reconcile with the server data.
  • An idempotent API survives double clicks, because repeating a request has no extra effect.
  • Do not use optimistic updates for high-stakes actions like payments.