Optimistic Updates With useOptimistic

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.

4 min read Level 3/5 #nextjs#react#optimistic
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.

Cookies in Server Actions →