Optimistic Updates
The problem
Not every change needs a server. Switching to dark mode or collapsing a sidebar only changes what your own screen looks like: the browser can apply it instantly, remember it locally, and the change is complete.
A like is different. Other people see the like count, it has to survive a refresh, and it has to show up when you open the app on your phone. A change like that is only real once the server has saved it, and reaching the server takes a round trip: a couple hundred milliseconds on a good connection, seconds on a bad one.
The obvious way to build the button respects that order. The user taps, you send the request, you show a spinner, and when the server answers you fill the heart. Now every tap has a built-in delay equal to the round trip. The app feels slow, and some users tap again because nothing seemed to happen.
The optimistic bet
An optimistic update flips the order. You update the screen immediately, as if the server had already said yes, and send the request in the background. It is called optimistic because you are assuming the request will succeed, and for an action like a like, that assumption is right nearly every time. The only extra work is the failure case: if the server says no, you put the screen back the way it was and tell the user.
When the server says yes
Click the heart and watch the count change before the request dot reaches the server. That reordering is the entire pattern.
Click the heart. The count updates before any request is sent.
In code, the optimistic half is just updating state before the request instead of after it:
function toggleLike() { setLiked(!liked); setCount(liked ? count - 1 : count + 1); api.setLike(postId, !liked);}This version works beautifully right up until a request fails. Then the heart stays filled, the count stays wrong, and the screen is lying to the user. That is why the optimistic half alone is not production ready.
When the server says no
The missing piece is rollback, and rollback rests on one habit: before you change anything, remember what it was. Here the server rejects every request. Click the heart and watch the update happen first and get taken back after the failure arrives.
Click the heart. The count updates first; the failure arrives after.
The production ready version snapshots the previous state, restores it in the catch block, and tells the user:
LikeButton.jsx
function LikeButton({ postId, initialLiked, initialCount,}) { const [liked, setLiked] = useState(initialLiked); const [count, setCount] = useState(initialCount); const [saving, setSaving] = useState(false); async function toggleLike() { if (saving) return; const previous = { liked, count }; setSaving(true); setLiked(!previous.liked); setCount(previous.count + (previous.liked ? -1 : 1)); try { await api.setLike(postId, !previous.liked); } catch { setLiked(previous.liked); setCount(previous.count); showToast('Could not save your like'); } finally { setSaving(false); } } return ( <button onClick={toggleLike} aria-pressed={liked}> <HeartIcon filled={liked} /> {count} </button> );}Three details matter here. The rollback restores the saved snapshot instead of guessing, so the interface ends up exactly where it started. The catch block tells the user, because a heart that quietly unfills looks like a bug. And the saving flag ignores taps while a request is in flight, so two overlapping requests can never fight over the snapshot.
You do not always have to hand write this. React ships useOptimistic for exactly this, and server state libraries build the same snapshot and rollback into their mutations: see TanStack Query and SWR. The state management tools article covers where those libraries fit.
When to use it
Optimistic updates fit actions that succeed almost every time and are easy to undo: likes, follows, toggles, renames, and reordering items in a list.
Skip them when a false yes is expensive: payments, deleting something you cannot restore, or anything the user would act on believing it succeeded. For those, wait for the server and show a pending state instead.
Why interviewers ask this
"What does the user see while the request is in flight?" is a standard follow up in system design interviews, and optimistic update with rollback is the expected answer for small, reversible actions. It shows you think about perceived speed and about failure at the same time. In the delivery framework, this is the heart of the data sending step.
The pattern also carries into bigger questions. Moving a card on a Jira board is the same bet as the like button, just with more state in the snapshot.
An optimistic update is a bet that the server will say yes. Only take the bet when you can pay it back: snapshot the previous state, restore it exactly on failure, and tell the user when you do.