useOptimistic lets a component show an instant, temporary value while a mutation runs, then converge with the real result when the action finishes. It turns a slow network call into immediate feedback: the UI flips now, and React reconciles the display once the server responds. It works with any transition, not just network calls, so the same pattern powers pending buttons and form feedback.
useOptimistic is stable since React 19. The setter only takes effect inside an Action, which is a function wrapped in startTransition or an Action prop.
The basic pattern
Call useOptimistic with the current value. It returns the optimistic state and a setter to change it for the duration of an Action.
import { useOptimistic, startTransition } from "react";
function LikeButton({ isLiked, toggleLike }) {
const [optimisticLiked, setOptimisticLiked] = useOptimistic(isLiked);
function handleClick() {
startTransition(async () => {
setOptimisticLiked(!optimisticLiked);
await toggleLike();
});
}
return (
<button onClick={handleClick}>
{optimisticLiked ? "Unlike" : "Like"}
</button>
);
}The button label flips the instant it is clicked, before the server call finishes. The parent updates the real isLiked prop on success, and the optimistic value converges to it in the same render, so there is no extra clearing step.
A disabled button during the request is the simplest version of the same idea. For the server call itself, see React Server Functions and Server Actions.
The value argument is what renders when nothing is pending, so it remains the source of truth after the action ends.
The setter runs inside an Action
The optimistic setter must be called inside an Action. Calling it in a plain event handler logs a warning and the value briefly appears, then reverts because nothing holds it.
function handleClick() {
startTransition(async () => {
setOptimisticLiked(true);
await toggleLike();
});
}startTransition holds the optimistic state until the async work finishes. Form action props already run inside a transition, so a setter called from a form action works without the wrapper. Treat the setter like a state update that only has meaning while a transition is open.
A Server Function that returns the saved value closes the loop between the guess and the answer, and the optimistic state simply hands over to that result.
Optimistic lists with a reducer
For lists, pass a reducer so React can reapply the pending change if the base list updates while the action runs.
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(current, newTodo) => [...current, { ...newTodo, pending: true }]
);
function handleAdd(text) {
const newTodo = { id: crypto.randomUUID(), text };
startTransition(async () => {
addOptimisticTodo(newTodo);
await saveTodo(newTodo);
});
}The new todo appears instantly with a pending flag. When saveTodo finishes, the parent replaces the list with the saved items, and the pending item resolves to the confirmed one.
The reducer matters because another update could change the list while this one is in flight, and React replays the reducer against the latest list. For the form variant that tracks pending state, see React 19 Form Actions and useActionState.
Rollback when the action fails
If the action throws, the optimistic value does not stick. The base value never changed, so React renders it again and the UI rolls back automatically.
startTransition(async () => {
removeItem(id);
try {
await deleteAction(id);
} catch (error) {
setError(error.message);
}
});The item reappears when the delete fails, and the error message shows why. Catch the error to give the user feedback instead of leaving them with a silently restored row.
Optimistic updates are a guess, so the failure path is part of the design rather than an edge case. Clear the error on the next attempt so the message disappears when the user retries.
Rollback is automatic because the base value never changed, so the optimistic state has nothing left to show.
When not to use it
useOptimistic only helps when the eventual truth is a value you already have, like a list or a flag. It does not replace loading states for work that has no optimistic projection, and it does not cache or fetch data on its own. See How to Handle Loading, Error, Empty, and Success States for the full async UI picture, and lean on a Server Function for the mutation itself.
Keep the projection close to the truth, and the optimistic UI will rarely need correction.
Rune AI
Key Insights
- useOptimistic returns optimistic state and a setter.
- The setter must run inside an Action.
- Use a reducer when the base state may change mid-action.
- A failed action rolls the optimistic state back.
- It pairs with Server Functions for instant form feedback.
Frequently Asked Questions
Where must I call the optimistic setter?
What happens if the server update fails?
Conclusion
useOptimistic shows a temporary value while a mutation runs and converges with the real state when the action finishes. Use a reducer for lists so concurrent updates stay correct.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.