Verified against Claude Code · 2026-08-01
Add an optimistic update that actually rolls back cleanly on failure
Implements an optimistic UI update for a specific action — a like, a save, a reorder — with a real rollback and error surface for the failure path, not just the happy-path demo most optimistic-update examples show.
The prompt
Ready to copy — highlighted parts are example details you can swap.
You are adding an optimistic update to a specific user action in a React app. The action should visually complete instantly, before the server confirms it, but you are equally responsible for the failure path — what the user sees and what state the app is left in when the server rejects the request. ACTION Clicking a heart icon to like a post in a feed — should fill in and the count should increment instantly on click. CURRENT CODE A PostCard component with a LikeButton child that calls an onLike prop, which fires a POST /posts/:id/like and only updates the UI after the response resolves. FAILURE BEHAVIOR Heart icon and count revert to their prior state, and a small inline toast reads "Couldn't like this post — try again." CONCURRENT UPDATE RISK The feed also polls for fresh like counts every 30 seconds — a poll landing mid-request could momentarily show a stale count before the like request resolves. IMPLEMENTATION RULES If this is a form submission wrapped in an Action, use useOptimistic to show the pending value merged over the real state; be precise about its actual mechanics — useOptimistic renders the optimistic value only while the associated action is in flight, and automatically reverts to whatever the real state resolves to once that action settles, whether it succeeded or failed. That automatic revert is not the same as a rollback with user feedback — it silently makes the optimistic UI disappear, so you must add explicit failure handling on top of it: an inline error message, a toast, or a visible undo state, so the user sees why their like count just dropped back down instead of experiencing an unexplained flicker. If this is not a form Action — a button click, a drag-drop reorder — implement the optimistic update manually: apply the new state immediately, fire the request, and on failure explicitly revert to a snapshot of the previous state taken before the optimistic update was applied, not to some assumed default. Guard against the concurrent-update risk named above: if a background refetch or a second user action could resolve after the optimistic update and overwrite it with stale data, decide explicitly whether the optimistic value or the fetched value should win, and implement whichever you chose — do not leave it to whichever happens to finish last by accident. Never let the UI sit in an ambiguous state where the user cannot tell whether their action succeeded, is still pending, or failed — one of those three must always be visually distinguishable. If the same action can be triggered again while a previous attempt is still in flight — a user double-clicking a like button, or tapping retry before the first request has resolved — decide explicitly whether to disable the control for the duration of the request or to let a second optimistic update stack on top of the first, and implement whichever choice you made rather than leaving both requests to race with no coordination between them. OUTPUT FORMAT 1. The implementation, as real code, covering the optimistic apply, the success path, and the failure path with rollback. 2. What exactly the user sees in each of the three states — optimistic/pending, confirmed success, and rolled-back failure. 3. How the concurrent-update risk is resolved, specifically, or a note that none exists for this action and why.
Customize
Optional — swap in your own details for the highlighted parts above.
Why this works
The precise mechanics of useOptimistic are the crux of this prompt, and they are widely misunderstood in exactly the direction that causes bugs: developers often assume it handles the failure case gracefully because it "reverts automatically," but that automatic revert is silent — it removes the optimistic value the instant the action settles, success or failure, with zero built-in user feedback, so a naive implementation that only calls useOptimistic and stops there produces a UI that flickers back to the old state on failure with no explanation, which reads to the user as a bug rather than an intentional rejection. Requiring an explicit pre-update snapshot for the manual, non-Action case targets the second most common mistake in hand-rolled optimistic updates: reverting to a hardcoded or assumed default value on failure instead of the actual prior state, which is wrong the moment the prior state wasn't the default — a like count that was already at 47 before the optimistic increment should roll back to 47 on failure, not to 0. Naming the concurrent-update risk explicitly and forcing a stated resolution matters because this is the class of bug that never shows up in a quick manual test — it only appears under real timing, when a background poll or a second tab's action happens to land during the optimistic window — and a prompt that doesn't ask about it will get an implementation that works perfectly in every demo and occasionally shows a genuinely wrong number in production. Forcing an explicit decision on double-triggering — disable versus let a second optimistic update stack — closes a related race that shares the same root cause as the concurrent-update risk: two in-flight requests for the same action with no coordination between them can resolve in either order, and whichever response lands second silently overwrites whatever the first one set, which for a like button might just flicker the count but for a reorder or a payment-adjacent action can leave the UI showing a result that neither request actually produced.
What you get back
function LikeButton({ postId, initialLiked, initialCount, onLikeRequest }) { const [state, setState] = useState({ liked: initialLiked, count: initialCount }); const [optimisticState, setOptimisticState] = useOptimistic(state); async function handleLike() { const snapshot = state; setOptimisticState({ liked: !state.liked, count: state.count + (state.liked ? -1 : 1) }); try { const confirmed = await onLikeRequest(postId); setState(confirmed); } catch { setState(snapshot); // explicit rollback to the real prior value, not a default showToast("Couldn't like this post — try again."); } } return <button onClick={handleLike} aria-pressed={optimisticState.liked}>{optimisticState.count}</button>; } Concurrent risk resolved: the 30-second poll's response is discarded (not applied to state) while a like request for the same post is in flight, tracked via a ref flag, so the poll can never stomp the optimistic value mid-request.
Verified against
Claude Code Sonnet 4.6 · 2026-08-01
Claude Sonnet 4.6 · 2026-08-05
Changelog
- 2026-08-01 — Initial publish, verified against Claude Code and Claude on Sonnet 4.6 with React 19.
Need this built into your business?
If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.
EXPLORE CUSTOM SOFTWARE
