Show the New State Instantly, Roll Back If It Fails
Optimistic Updates With useOptimistic
React's `useOptimistic` lets you display the new value immediately while a Server Action runs in the background. Revalidation reconciles to the real server state.
What you'll learn
- Wrap state with `useOptimistic`
- Update locally before the action resolves
- Let revalidation reconcile to the truth
Even a fast Server Action takes 50-200 ms round trip. For UI like a todo list, waiting
that long before showing the new item feels sluggish. useOptimistic lets you render
the imagined next state immediately.
The Hook
useOptimistic(realValue, reducer) returns [optimisticValue, addOptimistic]. The
reducer takes the current value and the optimistic action, and returns the next value.
'use client'
import { useOptimistic } from 'react'
import { addTodo } from './actions'
type Todo = { id: string; text: string; pending?: boolean }
export function TodoList({ todos }: { todos: Todo[] }) {
const [optimistic, addOpt] = useOptimistic(
todos,
(current, draft: Todo) => [...current, { ...draft, pending: true }],
)
async function action(form: FormData) {
const text = String(form.get('text') ?? '')
addOpt({ id: crypto.randomUUID(), text })
await addTodo(form)
}
return (
<>
<form action={action}>
<input name="text" />
<button>Add</button>
</form>
<ul>
{optimistic.map(t => (
<li key={t.id} style={{ opacity: t.pending ? 0.5 : 1 }}>{t.text}</li>
))}
</ul>
</>
)
} The new todo appears instantly with reduced opacity, then becomes solid after the action
returns and revalidation streams in the fresh todos prop.
Reconciliation
When the Server Action completes and revalidatePath fires, the parent Server Component
refetches and re-renders. The todos prop updates, and useOptimistic resets its
optimistic queue. If your imagined and real state disagree, the real one wins.
When It Fails
If the action throws, the optimistic value is discarded and the original state stays.
Surface the error via useActionState or the nearest error.tsx so the user knows.