Facebook Pixel

Optimistic UI Using useOptimistic

In this tutorial, we will learn how to use useOptimistic, one of the most powerful additions in React 19. Optimistic UI lets your app instantly update the interface before the server responds, creating a fast, smooth user experience.

What Is Optimistic UI?

Normally, when the user performs an action, like adding a comment, saving a task, or liking a post, we:

  1. Submit the form
  2. Wait for the server
  3. Update the UI after receiving the result

This makes the UI feel slow.

With optimistic UI:

React updates the UI instantly, assuming the server will succeed.

If the server later confirms the result, nothing changes. If the server errors, React can roll back the optimistic value.

This makes interactions feel fast and responsive.

Why React 19 Introduced useOptimistic

Before React 19:

  • Optimistic updates required manual state handling
  • We had to manage rollback logic ourselves
  • The code became messy and difficult to maintain

useOptimistic fixes this:

  • Handles optimistic updates safely
  • Works directly with Actions
  • Integrates with server transitions
  • Keeps code clean and declarative

React gives you both the optimistic value and the final server value.

Step 1: Create a Simple Server Action

We’ll simulate adding a new task.

app/actions/addTask.js
"use server";
 
export async function addTask(previousState, formData) {
  // Get the task text from the submitted form
  const text = formData.get("task");
 
  // simulate a slow network
  await new Promise(r => setTimeout(r, 1200));
 
  // In a real app, you would save to DB here
 
  // Return updated state (must match initial state shape: an array)
  return [
    ...previousState,
    { text }
  ];
}

This Action returns the new state (array of tasks) after a delay. Note that we include previousState as the first argument because useActionState passes it automatically.

Step 2: Build a Component with useOptimistic

Next.js allows client components to interact with server actions.

app/page.tsx

Add:

"use client";
 
import { useActionState, useOptimistic } from "react";
import { useFormStatus } from "react-dom";
import { addTask } from "./actions/addTask";
 
type Task = {
  text: string;
  optimistic?: boolean;
};
 
export default function TaskList() {
  const [tasks, action] = useActionState<Task[], FormData>(addTask, []);
 
  const [optimisticTasks, addOptimisticTask] = useOptimistic<
    Task[],
    string
  >(tasks, (currentTasks, newTaskText) => [
    ...currentTasks,
    { text: newTaskText, optimistic: true },
  ]);
 
  return (
    <div className="p-10 max-w-md mx-auto">
      <h1 className="text-xl font-bold mb-4">
        Optimistic UI with useOptimistic
      </h1>
 
      <form
        action={async (formData: FormData) => {
          const text = formData.get("task");
 
          if (typeof text !== "string" || !text.trim()) return;
 
          addOptimisticTask(text);
          await action(formData);
        }}
        className="space-y-3"
      >
        <input
          name="task"
          placeholder="Add a task"
          className="border p-2 rounded w-full text-black"
        />
        <SubmitButton />
      </form>
 
      <ul className="mt-6 space-y-2">
        {optimisticTasks.map((task, i) => (
          <li
            key={i}
            className={`p-2 border rounded ${
              task.optimistic
                ? "opacity-50 border-gray-300 bg-gray-100"
                : "bg-white"
            }`}
          >
            {task.text}
            {task.optimistic && (
              <span className="ml-2 text-xs text-gray-500">
                (Sending...)
              </span>
            )}
          </li>
        ))}
      </ul>
    </div>
  );
}
 
function SubmitButton() {
  const { pending } = useFormStatus();
 
  return (
    <button
      disabled={pending}
      className="bg-blue-600 text-white w-full p-2 rounded disabled:opacity-50"
    >
      {pending ? "Adding..." : "Add Task"}
    </button>
  );
}

How This Works Step by Step

1. useOptimistic(tasks, updaterFn)

You provide:

  • The real state (tasks)
  • An updater function describing how to update optimistically
(currentTasks: Task[], newTaskText: string) => [
  ...currentTasks,
  { text: newTaskText, optimistic: true }
]

React temporarily applies this optimistic value until the server responds.

2. When the form submits

action={async (formData: FormData) => {
  const text = formData.get("task");
  if (typeof text !== "string") return;
  
  // Update UI Instantly
  addOptimisticTask(text);
  
  // Send to server
  await action(formData);  
}}

You:

  1. Add the optimistic task immediately
  2. Call the real Server Action

The UI updates instantly — no waiting.

3. The optimistic UI displays differently

className={task.optimistic ? "opacity-50" : ""}

You visually mark items as “temporary” while waiting.

You could also:

  • Show a spinner
  • Disable editing
  • Style it differently

4. When the server returns

useActionState updates the real task list.

React automatically:

  • Merges the final result
  • Removes optimistic markers
  • Updates the UI cleanly

You write almost no additional logic.

Handling Errors (Rollback)

If your Action throws an error:

  • React automatically removes the optimistic state
  • Your UI reverts to the last confirmed server value

No manual rollback code is needed.

Example server error:

throw new Error("Network failed");

React:

  1. Shows optimistic task
  2. Receives error
  3. Removes it
  4. Keeps UI consistent

This is a major improvement over traditional optimistic patterns.

Real World Example: Liking a Post

const [optimisticLikes, likeOptimistically] = useOptimistic<number, number>(
  likes,
  (current, diff) => current + diff
);
 
<form
  action={async () => {
    likeOptimistically(1); // optimistic instant like
    await likePost();      // server Action
  }}
>
  <button>{optimisticLikes} Likes</button>
</form>

The like count updates instantly, even on slow networks.

When Should You Use useOptimistic?

Use it when:

  • Adding items (tasks, comments, posts)
  • Updating counts (likes, favorites, upvotes)
  • Toggling UI states (follow, subscribe)
  • Marking items complete
  • Removing items

Avoid it when:

  • Server results may drastically differ
  • The risk of incorrect optimism is high

Most modern apps rely on optimistic UI for speed.

useOptimistic is one of the most powerful tools in React 19. It lets your UI feel instant, responsive, and modern by updating immediately while the server work is still happening. Combined with Actions and useActionState, you can create smooth, bug-free forms and interactions with very little code. Next.js makes implementing this seamless by providing the full-stack infrastructure needed.