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:
- Submit the form
- Wait for the server
- 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:
- Add the optimistic task immediately
- 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:
- Shows optimistic task
- Receives error
- Removes it
- 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.