Optimistic UI Patterns in React 19
How useOptimistic and the new form actions change optimistic updates in React 19, with patterns for rollback and reconciliation that hold up.
Optimistic UI is the technique of showing the result of an action before the server confirms it. Click like and the heart fills instantly, even though the request is still in flight. Done well it makes an app feel weightless. Done badly it lies to the user and then snaps back when the server disagrees. React 19 ships a primitive, useOptimistic, that makes the good version much easier to build correctly.
Why optimistic updates are hard
The difficulty was never showing the optimistic state. It was reconciling it. You apply a temporary change, fire the request, and then you have to handle three outcomes: success, failure, and the arrival of fresh server data that may differ from your guess. Hand rolling this with useState means manually tracking pending items, remembering the previous value for rollback, and merging server truth back in. It is fiddly and easy to get subtly wrong.
Optimistic UI is not about showing a fake state. It is about showing a likely state and having a disciplined plan for when reality arrives.
It helps to see exactly where the hand rolled version goes wrong, because the failure is rarely in the happy path. The trouble starts when two optimistic actions overlap. A user likes a post, then immediately unlikes it before the first request resolves. With manual state you are now juggling two in flight changes, two saved previous values, and an ordering question about which server response is authoritative. Get the bookkeeping slightly wrong and the UI settles on the wrong final state, which is the worst possible outcome because the user trusts what they see. The value of a framework primitive here is not that it saves a few lines. It is that it removes the entire class of ordering and rollback bugs that you would otherwise have to reason about by hand for every concurrent action.
What useOptimistic gives you
useOptimistic lets you derive an optimistic view of state from a base value plus pending changes. The key insight in its design is that the optimistic state is temporary by construction. When the surrounding transition completes, React discards the optimistic value and falls back to the real state. You never manually roll back, because the optimistic layer was never the source of truth.
"use client";
import { useOptimistic } from "react";
function MessageList({ messages, sendMessage }) {
const [optimistic, addOptimistic] = useOptimistic(
messages,
(state, newMessage: string) => [
...state,
{ id: "temp", text: newMessage, pending: true },
]
);
async function action(formData: FormData) {
const text = formData.get("text") as string;
addOptimistic(text);
await sendMessage(text);
}
return (
<form action={action}>
<ul>
{optimistic.map((m) => (
<li key={m.id} style={{ opacity: m.pending ? 0.5 : 1 }}>
{m.text}
</li>
))}
</ul>
<input name="text" />
</form>
);
}
The optimistic array shows the new message immediately, dimmed to signal it is pending. When sendMessage resolves and the real messages prop updates, React drops the optimistic entry and the real one takes its place. If the action throws, the optimistic state is discarded and you are back to the confirmed list. No manual rollback code exists, because none is needed.
The reconciliation model
The mental model is a base state plus a reducer that layers pending changes on top. The base state is your real data, usually coming from the server. The reducer describes how an in flight change should look. React keeps the optimistic projection alive only for the duration of the transition that triggered it. This is why it composes so well with Server Actions: the action defines the boundary of the transition, and the optimistic state lives exactly as long as the action is pending.
The detail that surprises people coming from manual implementations is that the base state does not need to be patched when the action succeeds. You do not write the new message into the list yourself. The action mutates the server, the server data flows back down as a fresh prop, and the optimistic layer simply evaporates because the transition that owned it has ended. The real entry that arrives in the new messages prop takes the place the optimistic one was holding. This is what people mean when they say the optimistic state is temporary by construction: there is no moment where both the optimistic guess and the confirmed truth coexist and have to be merged. One ends exactly as the other begins, and React orchestrates that handoff for you.
A related subtlety is the pending flag on the temporary item. It is not magic from React, it is just a field you put in your reducer to mark the optimistic entry so the UI can style it differently. The dimming in the example is doing real communicative work: it tells the user this message is on its way but not yet confirmed, which sets the right expectation if the network is slow. When the confirmed message replaces it, the dimming disappears along with the temporary entry, and the transition from pending to confirmed reads as the message settling into place.
Handling failure honestly
Optimistic UI raises a real product question: what happens when the action fails. The pessimistic answer of doing nothing optimistic is safe but feels slow. The naive optimistic answer of silently reverting can confuse a user who saw their message appear and then vanish. The honest pattern is to revert and inform. Because useOptimistic reverts automatically, your job is the informing part.
async function action(formData: FormData) {
const text = formData.get("text") as string;
addOptimistic(text);
try {
await sendMessage(text);
} catch {
setError("Message failed to send. Try again.");
}
}
The optimistic entry disappears when the transition ends, and the error tells the user why. That combination respects both speed and truth.
Where optimistic UI fits and where it does not
Optimistic updates are ideal for actions that almost always succeed and whose result the client can predict: likes, toggles, adding an item to a list, reordering. They are a poor fit for actions whose outcome the client cannot guess, such as a payment that returns a server generated confirmation, or an operation whose result depends on server side logic you cannot replicate. For those, an honest pending state is better than an optimistic guess you will probably have to correct.
A useful test is to ask whether you can render the success state from information you already have on the client. If yes, optimistic UI is a natural fit. If the success state requires data only the server can produce, showing a pending state and waiting is the more truthful choice.
Pairing with transitions
useOptimistic is designed to live inside a transition, which is also what keeps the interaction responsive. The form action prop in React 19 wraps your handler in a transition automatically, which is why the examples above need no explicit startTransition. If you trigger an optimistic update outside a form action, wrap it in a transition yourself so React can manage the pending state and the optimistic lifetime together.
This coupling to transitions is also the reason optimistic UI and responsiveness end up being the same conversation. A transition keeps the triggering interaction from blocking the paint, so the optimistic update appears in the next frame rather than after the heavy work settles. If you forget the transition and call the optimistic update in a plain handler, you lose both the automatic lifetime management and the responsiveness guarantee at once, which is a good reason to lean on the form action prop wherever the interaction is a form submission. The framework is steering you toward the pattern that is correct on both axes.
Practical takeaways
- Reach for
useOptimisticwhen an action almost always succeeds and the client can render the success state from data it already has, like likes, toggles, reordering, and adding to a list. - Model the optimistic view as a base state plus a reducer that layers a pending change on top, and let React discard that layer when the transition ends rather than rolling back by hand.
- Mark optimistic entries with a
pendingfield and style them to communicate in flight status, so a slow network reads as expected rather than broken. - On failure, do not silently revert. The revert is automatic, so spend your effort on informing the user why the change disappeared.
- Keep optimistic updates inside a transition, which the form
actionprop does for you, so you get both automatic lifetime management and a responsive paint. - Choose an honest pending state instead of an optimistic guess for actions whose result only the server can produce, such as payments and server generated identifiers.
The payoff
React 19 turns optimistic UI from a careful manual dance into a declarative pattern. You describe the base state and how a pending change should look, and React handles the lifetime and the rollback. That shift matters because the manual version was exactly the kind of code that worked in the demo and broke in the edge cases. With the optimistic layer guaranteed to be temporary, you get the weightless feel of instant feedback without the class of bugs that used to come with it.