useOptimistic auto-reverts on failure — a hand-rolled optimistic hook doesn't
TIL why React's useOptimistic hook doesn't need explicit rollback code when a server call fails, while a manual optimistic-update hook built on plain useState does.
Today I learned why the optimistic-vote hook on Pollarise needed manual rollback code, and a hook built on useOptimistic wouldn't.
The original hook applies a vote to local state immediately, then reconciles with the server response:
const [localTally, setLocalTally] = useState<Tally | null>(null);
const vote = async (optionId: string) => {
setLocalTally((prev) => applyVote(prev, optionId));
const confirmed = await api.vote(pollId, optionId); // throws on rejection
setLocalTally(confirmed);
};If api.vote rejects — a duplicate vote, a closed poll — localTally is stuck showing the optimistic count forever, because nothing ever runs to undo applyVote. Fixing that with plain useState means capturing a snapshot before the update and restoring it in a catch, and getting careful about a second vote landing before the first one's rollback fires.
useOptimistic avoids this by never storing the optimistic value as committed state:
const [optimisticTally, setOptimisticTally] = useOptimistic(tally, applyVote);
const vote = (optionId: string) => {
startTransition(async () => {
setOptimisticTally(optionId);
const confirmed = await api.vote(pollId, optionId);
setTally(confirmed); // updates the real `tally` the hook is based on
});
};optimisticTally is computed from tally plus the pending transition on every render, not stored on its own. Once the transition settles — success or thrown error — React drops the optimistic value and optimisticTally falls back to tally. A failed vote just disappears; no snapshot, no catch block, no manual restore.
This only helps because the optimistic value is derived, not committed. If you need to show why a vote failed (not just revert it), you still need your own error state — useOptimistic erases the optimistic guess, it doesn't explain the rejection.
Related content
Pollarise: A Real-Time Social Polling Platform
Building the frontend for a fast-interaction polling platform from Figma designs, with local-first updates for high-frequency voting.
Turning Figma Designs Into Reusable React Components
TIL that building each screen straight from its Figma frame produced pixel-accurate but unreusable components — the fix was designing the component API from the design system's tokens, not from any one screen.
useDebounce hook
A small generic hook for debouncing a fast-changing value — search inputs, resize handlers, anything you don't want firing on every keystroke.