What useOptimistic Actually Saves You
Source: Dev.to
The problem with optimistic UI
A checkbox toggle should feel instant. When the toggle needs to persist to a server you have two choices:
- Wait for the response – the UI feels sluggish.
- Update immediately – optimistic UI, which feels faster for users but requires extra code to handle failures.
The manual optimistic‑UI implementation quickly accumulates state variables, flags, and try/catch blocks, especially as the component grows more complex.
Manual optimistic implementation
function TodoItem({ todo, onToggle }) {
const [checked, setChecked] = useState(todo.completed);
const [pending, setPending] = useState(false);
async function handleToggle() {
setChecked((prev) => !prev);
setPending(true);
try {
await onToggle(todo.id, !checked);
} catch {
setChecked(checked); // rollback on error
} finally {
setPending(false);
}
}
return (
{todo.title}
);
}
- Lines: 26
- Edge cases:
- Component unmounts while the request is in flight.
- Parent updates
todo.completedfrom another source while the toggle is pending, causing the localcheckedstate to drift from the prop. - Each edge case adds more state and branching logic.
Using useOptimistic with startTransition
function TodoItem({ todo, onToggle }) {
const [optimisticChecked, addOptimistic] = useOptimistic(
todo.completed,
(state, next) => next,
);
function handleToggle() {
startTransition(async () => {
const next = !todo.completed;
addOptimistic(next); // optimistic update
await onToggle(todo.id, next);
});
}
return (
{todo.title}
);
}
- Lines: 23 – only a few fewer, but the real savings are mental, not textual.
Why it saves you
startTransitionwraps the async work in a React Transition. When the transition finishes (whether it succeeds or fails), React re‑renders the component.useOptimisticsimply mirrorstodo.completed.- If the parent updates the prop (success), the checkbox stays checked.
- If the parent does not update the prop (failure), the checkbox automatically rolls back to the previous value.
- No separate “pending” flag, no extra
try/catchfor state reset – the prop remains the single source of truth.
Error handling still matters
useOptimistic takes care of the UI state, but you still need to:
- Show an error toast or notification.
- Log the failure.
- Offer a retry mechanism.
These concerns live outside the optimistic‑state logic.
A broader pattern
Any code that interacts with an external source (server, API, user action) faces uncertainty. You can either:
- Let the uncertainty propagate through many functions/components.
- Contain it at the boundary where it enters your system.
- Zod does this for server‑side data: validate at the endpoint, then downstream code works with clean, typed data.
useOptimisticdoes the same for UI state: declare the optimistic change right where the user acts (insidestartTransition). The hook keeps the UI in sync with the prop, so downstream components only render.
Result: one clear boundary, one place to think about the optimistic update, and a single point to debug.
👉 Try it in practice: useOptimistic