useOptimistic renders a temporary value while an asynchronous action is in flight, so the interface reflects the user’s intent immediately. When the action settles, React discards the optimistic value and re-renders from the real state, which is either the new server data or the previous value on failure.
Before you start
You should be comfortable with state, async actions and useTransition. This article assumes React 19-style actions; the reconciliation idea applies to any optimistic UI.
Step-by-step walkthrough
Step 1: Keep real state as the source of truth
Pass the actual state to useOptimistic(state, reducer); the hook returns the optimistic view plus an updater. The real state never changes optimistically, so a failed action simply stops applying the update and the UI returns to reality.
Step 2: Apply the optimistic update inside the action
Call the updater inside the action callback, not during render. The update is scoped to the transition: while the action is pending the optimistic value is shown, and the value is reset automatically once the action completes or fails.
Step 3: Make the action idempotent and recover from failure
If the server call throws, React reverts to the base state, so the UI is consistent without manual rollback. Report the error separately, and ensure the action is safe to retry, since the user may repeat it after seeing a failure.
Worked scenario
The list shows the new item instantly and removes it if the action fails.
function TodoList({ todos, addTodo }) {
const [optimistic, addOptimistic] = useOptimistic(todos, (state, text) => [
...state,
{ text, pending: true },
]);
async function handleAdd(formData) {
addOptimistic(String(formData.get('text')));
await addTodo(String(formData.get('text')));
}
return (
<form action={handleAdd}>
<input name="text" />
<ul>{optimistic.map((t) => <li key={t.text}>{t.text}</li>)}</ul>
</form>
);
}Walk through the example
Submitting the form calls handleAdd, which first adds a pending item to the optimistic list, so it appears at once. Then addTodo runs; when it resolves, the real todos prop updates and the optimistic value is dropped. If addTodo rejects, the optimistic item disappears without any manual cleanup.
Common mistake
Treating the optimistic value as the source of truth and reading it elsewhere, which shows data the server has not accepted. Another is adding an optimistic item with a key that changes on reconciliation, causing a remount; use a stable key.
Verify the behavior
Delay the server response and confirm the item shows immediately with a pending style. Make the action reject and confirm the item vanishes. Submit the same item twice and confirm no duplicate remains once the real state arrives.
Interview exercise
Why does useOptimistic revert automatically instead of needing manual rollback?
Answer and reasoning
Because it never mutates the real state. The optimistic value exists only for the duration of the transition, layered on top of the base state. When the action finishes, the layer is discarded and the component renders the real state again, which is the server result on success or the old value on failure.
Continue learning
Compare pending UI in React transition urgency and state snapshots. Read the React useOptimistic reference and try the React interview questions.