useOptimistic
Shows a different state while an async action is underway, providing instant feedback by optimistically updating the UI before the server confirms the change (React 19+).
Signature
const [optimisticState, addOptimistic] = useOptimistic<S, A>(state: S, updateFn?: (currentState: S, optimisticValue: A) => S)Parameters
| Parameter | Type | Description |
|---|---|---|
| state | S | The actual state value. The optimistic state reverts to this when no action is pending. |
| updateFn | (currentState: S, optimisticValue: A) => S | Optional pure function that merges current state with the optimistic value to produce the optimistic state. |
Return Value
A tuple of [optimisticState, addOptimistic]. optimisticState is the current value (actual or optimistic). addOptimistic triggers an optimistic update.
Examples
import { useOptimistic, useActionState } from 'react';
function LikeButton({ initialLikes, postId }: { initialLikes: number; postId: string }) {
const [likes, submitAction, isPending] = useActionState(
async (prev: number) => {
await fetch(`/api/posts/${postId}/like`, { method: 'POST' });
return prev + 1;
},
initialLikes,
);
const [optimisticLikes, addOptimisticLike] = useOptimistic(
likes,
(current: number) => current + 1,
);
return (
<form action={() => {
addOptimisticLike(null);
submitAction();
}}>
<button type="submit">
❤️ {optimisticLikes}
</button>
</form>
);
}import { useOptimistic } from 'react';
interface Todo { id: string; text: string; sending?: boolean }
function TodoList({ todos, addTodo }: {
todos: Todo[];
addTodo: (text: string) => Promise<void>;
}) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic<Todo[], string>(
todos,
(state, newText) => [
...state,
{ id: 'temp-' + Date.now(), text: newText, sending: true },
],
);
async function handleSubmit(formData: FormData) {
const text = formData.get('text') as string;
addOptimisticTodo(text);
await addTodo(text);
}
return (
<div>
<form action={handleSubmit}>
<input name="text" placeholder="Add todo..." />
<button type="submit">Add</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.sending ? 0.6 : 1 }}>
{todo.text} {todo.sending && '(saving...)'}
</li>
))}
</ul>
</div>
);
}import { useOptimistic } from 'react';
interface Message {
id: string;
text: string;
status: 'sent' | 'delivered' | 'sending';
}
function Chat({ messages, sendMessage }: {
messages: Message[];
sendMessage: (text: string) => Promise<void>;
}) {
const [optimisticMessages, addOptimistic] = useOptimistic<Message[], string>(
messages,
(state, newText) => [
...state,
{ id: `temp-${Date.now()}`, text: newText, status: 'sending' as const },
],
);
async function handleSend(formData: FormData) {
const text = formData.get('message') as string;
addOptimistic(text);
await sendMessage(text);
}
return (
<div>
{optimisticMessages.map(msg => (
<div key={msg.id} style={{ opacity: msg.status === 'sending' ? 0.5 : 1 }}>
{msg.text}
<span style={{ fontSize: 10, marginLeft: 8 }}>
{msg.status === 'sending' ? '⏳' : msg.status === 'delivered' ? '✓✓' : '✓'}
</span>
</div>
))}
<form action={handleSend}>
<input name="message" />
<button type="submit">Send</button>
</form>
</div>
);
}Common Pitfalls
Not reverting optimistic state on error — if the server action fails, ensure the optimistic update doesn't persist.
Using useOptimistic outside of a transition or form action — it works best with startTransition or form actions.
Making the updateFn impure (e.g., calling Math.random() or Date.now()) — it should be a pure merge function.
Overcomplicating optimistic state for simple loading indicators — sometimes isPending from useTransition is enough.
Understanding useOptimistic
useOptimistic enables instant UI feedback by temporarily showing a predicted state while an async operation is in progress. When a user likes a post, adds an item to a list, or sends a message, the UI updates immediately without waiting for the server response. If the operation succeeds, the optimistic state transitions smoothly to the real state. If it fails, React automatically reverts to the pre-optimistic state.
The hook accepts the current "real" state and an optional update function. When you call addOptimistic with a value, React runs your update function with the current state and your optimistic value, producing a temporary state that's shown to the user. When the async action completes (the enclosing transition or form action finishes), React reverts to using the real state, which should now reflect the server's response.
The revert mechanism is automatic and tied to React's transition system. When useOptimistic is used inside a form action or startTransition, React knows when the async operation completes and automatically stops showing the optimistic state. This means you don't need to manually manage "pending" vs "confirmed" states — React handles the lifecycle for you.
A common pattern is marking optimistic items with a visual indicator (reduced opacity, a spinning icon, or a "sending" label) so users understand the action hasn't been confirmed yet. The update function can add metadata like a sending flag to distinguish optimistic entries from confirmed ones. This transparency helps set user expectations while still providing the snappy, instant feedback that makes modern applications feel responsive.
Related Hooks
useActionStateManages form action state with built-in pending tracking. Combines the action function pattern with automatic state management for form submissions (React 19+).
useFormStatusReads the status of the parent form during submission. Provides pending state, form data, HTTP method, and action URL from within any component nested inside a <form>.
useTransitionMarks a state update as non-urgent, allowing the UI to remain responsive during the transition. Returns a pending flag and a function to wrap low-priority updates.
More Other Hooks
Explore All React Hooks
Browse our complete reference of 19 React hooks with signatures, examples, pitfalls, and in-depth explanations.