useOptimistic Hook
Show an optimistic (predicted) state while an async action is in progress, then reconcile when it completes.
Search across all documentation pages
Show an optimistic (predicted) state while an async action is in progress, then reconcile when it completes.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Quick-reference recipe card - copy-paste ready.
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(currentState, newMessage: string) => [
...currentState,
{ text: newMessage, sending: true },
]
);
// Call inside an action or transition
startTransition(() => {
addOptimistic("Hello!");
await sendMessage("Hello!");
});When to reach for this: You want the UI to update instantly when a user performs an action (like sending a message, liking a post, or adding an item) while the server processes the request in the background.
"use client";
import { useOptimistic, useActionState, useRef } from "react";
interface Message {
id: number;
text: string;
sending?: boolean;
}
async function sendMessageAction(
prevState: Message[],
formData: FormData
): Promise<Message[]> {
const text = formData.get("message") as string;
// Simulate server delay
await new Promise((resolve) => setTimeout(resolve, 1500));
return [
...prevState,
{ id: Date.now(), text, sending: false },
];
}
export function Chat() {
const [messages, formAction, isPending] = useActionState(sendMessageAction, [
{ id: 1, text: "Welcome to the chat!", sending: false },
]);
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(state, newMessage: string) => [
...state,
{ id: Date.now(), text: newMessage, sending: true },
]
);
const formRef = useRef<HTMLFormElement>(null);
async function handleSubmit(formData: FormData) {
const text = formData.get("message") as string;
if (!text.trim()) return;
formRef.current?.reset();
addOptimistic(text);
await formAction(formData);
}
return (
<div className="space-y-3 max-w-sm">
<ul className="space-y-2">
{optimisticMessages.map((msg) => (
<li
key={msg.id}
className={`text-sm px-3 py-2 rounded ${
msg.sending
? "bg-gray-100 text-gray-400 italic"
: "bg-blue-50 text-gray-900"
}`}
>
{msg.text}
{msg.sending && <span className="ml-2 text-xs">(sending...)</span>}
</li>
))}
</ul>
<form ref={formRef} action={handleSubmit} className="flex gap-2">
<input
name="message"
className="flex-1 border rounded px-3 py-2"
placeholder="Type a message..."
required
/>
<button
type="submit"
disabled={isPending}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
Send
</button>
</form>
</div>
);
}What this demonstrates:
useOptimistic takes the actual state and an update function that describes how to apply an optimistic changeaddOptimistic(value), React immediately shows the merged optimistic state| Parameter | Type | Description |
|---|---|---|
state | T | The actual (source-of-truth) state value |
updateFn | (currentState: T, optimisticValue: V) => T | Pure function that merges the optimistic value into the current state |
| Return | Type | Description |
|---|---|---|
optimisticState | T | Current state with optimistic updates applied (equals state when no action is pending) |
addOptimistic | (value: V) => void | Function to trigger an optimistic update |
Optimistic like button:
const [optimisticLikes, addLike] = useOptimistic(
likes,
(current, _: null) => current + 1
);
async function handleLike() {
startTransition(async () => {
addLike(null);
await likePost(postId);
});
}
return (
<button onClick={handleLike}>
{optimisticLikes} Likes
</button>
);Optimistic todo toggle:
const [optimisticTodos, toggleOptimistic] = useOptimistic(
todos,
(state, toggledId: number) =>
state.map((todo) =>
todo.id === toggledId ? { ...todo, done: !todo.done } : todo
)
);Optimistic delete:
const [optimisticItems, removeOptimistic] = useOptimistic(
items,
(state, removedId: string) => state.filter((item) => item.id !== removedId)
);// The generic types are inferred from parameters
const [optimistic, add] = useOptimistic(
messages, // T = Message[]
(state, text: string) => ... // V = string
);
// add: (value: string) => void
// optimistic: Message[]
// Explicit generics when needed
const [optimistic, add] = useOptimistic<Todo[], number>(
todos,
(state, toggledId) => state.map(t =>
t.id === toggledId ? { ...t, done: !t.done } : t
)
);Calling addOptimistic outside a transition - The optimistic state only works correctly inside an async action or startTransition. Outside, the overlay is removed immediately. Fix: Always call addOptimistic inside startTransition or a form action.
Mutating state in updateFn - Mutating the current state array (e.g., state.push(item)) causes bugs. Fix: Return a new array or object from updateFn.
No error callback - There is no built-in way to show an error toast when the optimistic state rolls back. Fix: Handle errors in your action function and update state with an error message.
Multiple rapid optimistic updates - Each call to addOptimistic applies on top of the previous optimistic state, which can lead to unexpected results if the update function is not composable. Fix: Design your updateFn to be idempotent or additive.
Stale actual state - If the actual state prop changes from a different source while an action is pending, the optimistic overlay recalculates on top of the new actual state. Fix: This is usually the correct behavior, but be aware of it when debugging.
| Alternative | Use When | Don't Use When |
|---|---|---|
useState with manual rollback | You need custom rollback logic or error handling | Simple optimistic patterns where auto-rollback suffices |
useTransition with isPending | You only need a loading indicator, not an optimistic value | You want the UI to reflect the expected result immediately |
TanStack Query onMutate | You use TanStack Query and need optimistic updates with cache invalidation | You're using server actions and want a lightweight solution |
| Disable and spinner | Simplicity is preferred and latency is low | Users expect instant feedback |
Why useOptimistic over manual state? useOptimistic automatically rolls back on failure and merges correctly with the actual state when the action completes. Manual implementations are error-prone and verbose.
startTransition or a form action to keep the optimistic state visible.useOptimistic automatically rolls back on failure and merges correctly when the actual state updates.useOptimistic is less error-prone for common patterns like adding, toggling, or deleting items.state.push(item)) causes bugs because React expects immutable updates.updateFn.// Wrong
(state, newItem) => { state.push(newItem); return state; }
// Correct
(state, newItem) => [...state, newItem]updateFn is composable -- each call should produce a valid result when layered.const [optimistic, add] = useOptimistic(
messages, // T = Message[]
(state, text: string) => [ // V = string
...state,
{ id: Date.now(), text, sending: true },
]
);
// add: (value: string) => void
// optimistic: Message[]const [optimisticLikes, addLike] = useOptimistic(
likes,
(current, _: null) => current + 1
);
async function handleLike() {
startTransition(async () => {
addLike(null);
await likePost(postId);
});
}useOptimistic when users expect instant feedback (messages, likes, toggles).useOptimistic overlaysReviewed by Chris St. John·Last updated Jul 16, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥