For lists where the user adds, removes, or edits items, you don't want to wait for the server round-trip before showing the change. useOptimistic lets you render the predicted next state instantly.
1"use client";
2import { useOptimistic } from "react";
3import { addTodo } from "./actions";
4
5type Todo = { id: string; text: string; pending?: boolean };
6
7export function TodoList({ initial }: { initial: Todo[] }) {
8 const [optimistic, addOptimistic] = useOptimistic(
9 initial,
10 (state, next: Todo) => [...state, { ...next, pending: true }],
11 );
12
13 async function action(formData: FormData) {
14 const text = formData.get("text")?.toString() ?? "";
15 addOptimistic({ id: crypto.randomUUID(), text }); // instant!
16 await addTodo(text); // confirms or rolls back
17 }
18
19 return (
20 <>
21 <form action={action}>
22 <input name="text" required />
23 <button>Add</button>
24 </form>
25 <ul>
26 {optimistic.map((t) => (
27 <li key={t.id} className={t.pending ? "opacity-50" : ""}>{t.text}</li>
28 ))}
29 </ul>
30 </>
31 );
32}addOptimistic(newTodo) is called → optimistic is the predicted state.If the action fails, the optimistic state is discarded automatically.
1const [optimistic, removeOptimistic] = useOptimistic(
2 items,
3 (state, idToRemove: string) => state.filter((x) => x.id !== idToRemove),
4);
5
6async function onDelete(id: string) {
7 removeOptimistic(id);
8 await deleteItem(id);
9}1const [optimistic, updateOptimistic] = useOptimistic(
2 items,
3 (state, patch: { id: string; fields: Partial<Item> }) =>
4 state.map((x) => (x.id === patch.id ? { ...x, ...patch.fields } : x)),
5);
6
7async function rename(id: string, name: string) {
8 updateOptimistic({ id, fields: { name } });
9 await renameItem(id, name);
10}For everything else — likes, todos, comments, list reorderings — optimistic UI is a UX upgrade with very little code.