Design a like (or save/follow) button that feels instant. Apply RADIO; cover rollback and rapid clicks.
Optimistic updates, exact rollback, request dedup, and idempotency for high-frequency micro-interactions.
Requirements: a toggle that feels instant, ends in the correct state, survives failure and rapid clicks, and is accessible. Architecture: on click, immediately update local UI state (optimistic), fire the request in the background, and reconcile when it resolves — on error, roll back to the previous value and surface a subtle notice. Data model: keep the displayed value plus the last server-confirmed value so rollback is exact; track an in-flight flag per item. Interface: POST /like { id, liked } returning the authoritative state/count. Optimizations: dedup or cancel concurrent requests for the same item; debounce rapid toggles or send only the final intent; keep the control clickable but reflect pending state; make it an accessible toggle (aria-pressed). Why optimistic: perceived latency dominates UX for micro-interactions (like, star, follow) — waiting a round-trip makes them feel broken. The correctness trap is reconciliation: store the pre-update value so a failed request restores exactly it, not a guessed value, and handle the race where the user toggles twice before the first response returns (sequence responses, or cancel superseded requests). Prefer sending the desired END-STATE rather than a delta so retries are idempotent. Roll back visibly but gently — revert the icon and show a toast — so the user knows it didn't persist.
Like/star/follow/save toggles, and any high-frequency action where a round-trip wait would feel broken.
function LikeButton({ id, liked, count, save }) {
const [state, setState] = React.useState({ liked, count });
async function toggle() {
const prev = state; // remember for rollback
const next = { liked: !prev.liked,
count: prev.count + (prev.liked ? -1 : 1) };
setState(next); // optimistic
try {
const server = await save(id, next.liked); // send end-state (idempotent)
setState({ liked: server.liked, count: server.count });
} catch {
setState(prev); // rollback to exact prior value
}
}
return (
<button aria-pressed={state.liked} onClick={toggle}>
♥ {state.count}
</button>
);
}