Interview question
Implement an optimistic favorite with rollback
Applies an immediate reversible mutation, blocks overlapping writes, and restores the previous state on failure.
TL;DR
Applies an immediate reversible mutation, blocks overlapping writes, and restores the previous state on failure.
Optimistic state, rollback, synchronous pending guards, server truth, and user feedback.
Practice the problem like a real interview: restate, reason, implement, and test.
Build an accessible Favorite button for one drill. Activating it must update the icon and aria-pressed immediately, send the desired final boolean to the API, and allow only one mutation at a time. A validation rejection must restore the captured previous value and announce a recoverable error. If the request outcome is uncertain, reconcile with the server before presenting the final state as confirmed.
The drill starts not favorited. One activation immediately changes the command to Remove favorite and begins PUT /drills/42/favorite with { favorite: true }. A second activation during that request does nothing. On success, the server-returned value becomes canonical. On a rejected mutation, the UI returns to Add favorite, announces Favorite was not saved, and becomes actionable again.
pending.aria-pressed, pending state, and error announcement understandable to assistive technology.finally and verify that duplicate activation, normalization, rollback, and reconciliation are all observable.import { useRef, useState } from "react";
type FavoriteApi = {
setFavorite: (drillId: string, favorite: boolean) => Promise<{ favorite: boolean }>;
getFavorite: (drillId: string) => Promise<{ favorite: boolean }>;
};
type ApiError = Error & { status?: number };
export function FavoriteButton({
drillId,
initialFavorite,
api,
}: {
drillId: string;
initialFavorite: boolean;
api: FavoriteApi;
}) {
const [favorite, setFavorite] = useState(initialFavorite);
const [pending, setPending] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const mutationInFlight = useRef(false);
async function changeFavorite(desired: boolean) {
if (mutationInFlight.current) return;
mutationInFlight.current = true;
const previous = favorite;
setFavorite(desired);
setMessage(null);
setPending(true);
try {
const saved = await api.setFavorite(drillId, desired);
setFavorite(saved.favorite);
} catch (caught) {
setFavorite(previous);
const error = caught as ApiError;
const knownRejection = error.status !== undefined && error.status < 500;
if (knownRejection) {
setMessage("Favorite was not saved. Try again.");
} else {
setMessage("Connection was lost. Checking the saved value...");
try {
const current = await api.getFavorite(drillId);
setFavorite(current.favorite);
setMessage("Favorite status was refreshed.");
} catch {
setMessage("Favorite status is uncertain. Refresh and try again.");
}
}
} finally {
mutationInFlight.current = false;
setPending(false);
}
}
return (
<div>
<button
type="button"
aria-pressed={favorite}
disabled={pending}
onClick={() => void changeFavorite(!favorite)}
>
{pending ? "Saving favorite..." : favorite ? "Remove favorite" : "Add favorite"}
</button>
<p role="status" aria-live="polite">{message}</p>
</div>
);
}
This deliberately serializes writes. Supporting rapid toggles is a different contract: queue or coalesce desired state, attach mutation versions, and define how late responses are rejected or reconciled.
The local mutation path is constant time and space. The important cost is semantic: optimistic UI improves perceived latency, but rollback and uncertain outcomes add state transitions that must be designed and tested. It fits fast, reversible, low-conflict actions such as favorites. Payments, destructive operations, scarce inventory, and permission changes usually need server confirmation or a much stronger conflict protocol. Serializing one write is simple and predictable; allowing rapid toggles requires versioning or coalescing rather than merely removing disabled.
aria-pressed changes before the controlled API promise resolves.getFavorite; verify reconciliation controls the final UI.pending leaves a same-tick window; acquire a ref guard synchronously.aria-pressed, button text, and status feedback were omitted.Move to the linked follow-up, next path step, prerequisite, or deeper variant.
Practice the next layer of the same subject.