DEV Community

Atlas Whoff
Atlas Whoff

Posted on

React 19 useOptimistic: Build Instant UI Without Waiting for the Server

The most underused React 19 hook is useOptimistic. Most teams still reach for local state + loading spinners when they could ship instant UI.

Here's the hook, the patterns, and the edge cases that bite in production.

What useOptimistic Solves

// Old way — users see lag
async function addTodo(text: string) {
  setLoading(true);
  const newTodo = await createTodo(text); // 200-800ms wait
  setTodos(prev => [...prev, newTodo]);
  setLoading(false);
}
Enter fullscreen mode Exit fullscreen mode

With useOptimistic:

const [optimisticTodos, addOptimisticTodo] = useOptimistic(
  todos,
  (currentTodos, newText: string) => [
    ...currentTodos,
    { id: crypto.randomUUID(), text: newText, pending: true }
  ]
);

async function addTodo(text: string) {
  addOptimisticTodo(text);           // Instant UI update
  const newTodo = await createTodo(text); // Background
  setTodos(prev => [...prev, newTodo]);
}
Enter fullscreen mode Exit fullscreen mode

Item appears instantly. If the request fails, the optimistic update rolls back automatically.

The API

const [optimisticState, dispatchOptimistic] = useOptimistic(
  state,     // real state (from server/parent)
  updateFn   // (currentState, action) => nextOptimisticState
);
Enter fullscreen mode Exit fullscreen mode

React automatically reverts optimisticState back to state when the async action finishes — success or failure. No manual rollback.

Pattern 1: Form Actions

function TodoList({ todos }: { todos: Todo[] }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, text: string) => [
      ...state,
      { id: crypto.randomUUID(), text, completed: false, pending: true }
    ]
  );

  async function formAction(formData: FormData) {
    addOptimisticTodo(formData.get("todo") as string);
    await saveTodo(formData.get("todo") as string);
  }

  return (
    <>
      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id} style={{ opacity: todo.pending ? 0.6 : 1 }}>
            {todo.text}
          </li>
        ))}
      </ul>
      <form action={formAction}>
        <input name="todo" /><button type="submit">Add</button>
      </form>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Pattern 2: Likes / Toggles

const [optimistic, dispatch] = useOptimistic(
  { liked, count },
  (current) => ({
    liked: !current.liked,
    count: current.liked ? current.count - 1 : current.count + 1,
  })
);

async function handleLike() {
  dispatch(null);  // instant toggle
  const result = await toggleLike(postId);
  setLike(result);
}
Enter fullscreen mode Exit fullscreen mode

The Critical Rollback Rule

Automatic rollback only fires when the async action throws. Silent failures stick:

// BAD — optimistic state gets stuck
async function save(data) {
  const json = await fetch(...).then(r => r.json());
  if (!json.success) return json.error; // returns, doesn't throw
}

// GOOD — rollback fires
async function save(data) {
  const res = await fetch(...);
  if (!res.ok) throw new Error(`Failed: ${res.status}`);
  return res.json();
}
Enter fullscreen mode Exit fullscreen mode

Error State Pattern

async function submitComment(formData: FormData) {
  const text = formData.get("comment") as string;
  setError(null);
  addOptimisticComment(text);

  try {
    const comment = await postComment(postId, text);
    setComments(prev => [...prev, comment]);
  } catch {
    setError("Failed to post. Try again.");
    // useOptimistic auto-reverts the optimistic entry
  }
}
Enter fullscreen mode Exit fullscreen mode

When NOT to Use It

  • Server response changes data significantly — optimistic placeholder flashes when replaced
  • Conflicts are likely — collaborative editing, inventory, financial writes
  • Action is irreversible — deletions, charges, sent emails — show confirmation instead

The Real Win

useOptimistic doesn't make your server faster — it makes your app feel faster by showing results before the server responds. For most CRUD operations that's the only latency that matters to users.

Use for: adds, likes, toggles, reorders, soft deletes.
Skip for: irreversible actions, conflict-prone writes, server-computed transforms.


React 19 + Next.js Server Actions + optimistic UI pre-wired? The AI SaaS Starter Kit ships with all of it — skip the 40-hour setup.

Top comments (0)