
Optimistic UI Updates
Optimistic updates apply changes to the UI immediately, before the server confirms them. On failure, you roll back. This makes mutations feel instant.
The Pattern
function TodoList() {
const [todos, setTodos] = useState(initialTodos);
async function addTodo(text) {
const optimisticTodo = { id: crypto.randomUUID(), text, status: "pending" };
// Update UI immediately
setTodos((prev) => [...prev, optimisticTodo]);
try {
const savedTodo = await api.createTodo(text);
// Replace the optimistic entry with the server response
setTodos((prev) =>
prev.map((t) => (t.id === optimisticTodo.id ? savedTodo : t)),
);
} catch (err) {
// Roll back on failure
setTodos((prev) => prev.filter((t) => t.id !== optimisticTodo.id));
showErrorToast("Failed to add todo");
}
}
} React 19: useOptimistic
React 19 provides a hook designed for this pattern:
const [optimisticTodos, addOptimistic] = useOptimistic(
todos,
(state, newTodo) => [...state, newTodo],
);
async function handleAdd(text) {
addOptimistic({ id: "temp", text, pending: true });
await api.createTodo(text); // revalidation happens after this
}
// Render optimisticTodos — reflects pending state automatically useOptimistic reverts the optimistic state when the server response arrives.
TanStack Query Integration
const mutation = useMutation({
mutationFn: (text) => api.createTodo(text),
onMutate: async (text) => {
await queryClient.cancelQueries({ queryKey: ["todos"] });
const previous = queryClient.getQueryData(["todos"]);
queryClient.setQueryData(["todos"], (old) => [
...old,
{ id: "temp", text, pending: true },
]);
return { previous };
},
onError: (err, text, context) => {
queryClient.setQueryData(["todos"], context.previous);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["todos"] });
},
}); When Optimistic Updates Work
- Low failure rate operations (adding a like, toggling a setting)
- User has reliable connectivity
- The operation is idempotent or easy to reverse visually
When They Don’t
- Operations that frequently fail (form submissions with server validation)
- Irreversible high-stakes actions (payments, permanent deletions — use confirmation dialogs instead)
- When you need the server’s response to continue (e.g., a generated ID for the next step)









