Show it now, settle to truth
Show changes instantly, then let the server's answer replace them.
Sam Selikoff@samselikoffThere's a really neat property of useOptimistic that didn't click for me until @acdlite explained it during my React Conf talk prep: when your app is settled, it's guaranteed to render the *exact* same UI as if you had never called useOptimistic at all. This is a little demo showing how useOptimistic "layers in" to your existing code. And this is really how it should be wielded. The before state has all the correct logic and behavior: the buttons reorder and delete the items, the output is always consistent, and the UI stays responsive to new actions at all times. But it's sluggish. Calling useOptimistic gives you a `setOptimisticTodos` function that you can use to update your UI immediately while the actions are in flight. By rendering the list from `optimistic` instead of `todos`, you get a bomb shelter where you get to go to town on your client-side state to show immediate updates. But here's the kicker: as soon as all the pending actions settle and your server-side data revalidates, the page component is re-rendered, and *all* of your optimistic updates are thrown away. That's why when the app settles, you're guaranteed to have a UI that matches what you'd have if you hit +R. No long-lived client-side cache to manage, no merging or deduping in-flight data with potentially stale React state. No bookkeeping. Just like setState + re-render let us completely stop worrying about how our app goes from state s1 to s2 – and instead just throws it all away and re-renders everything from scratch – useOptimistic lets us temporarily fork our client-side state while server actions are in flight, make some ephemeral changes, but then reverts to the server-side source of truth continuously as our users interact with our app. It's an extremely forgiving programming model that leaves you feeling confident that you won't hit any surprises as you layer in richer interactivity.
1 · Waiting feels slow
Delete an item. The server takes 600ms to answer.
Wait for the serverBad
pending
0Optimistic layerGood
pending
02 · A copy drifts
Tick the reject box, delete an item, and compare the server list.
Separate client copyBad
pending
0Layer over serverGood
pending
0const [optimisticTodos, removeOptimistic] = useOptimistic(
todos, // server data, the source of truth
(list, id) => list.filter((t) => t.id !== id),
);
async function remove(id) {
startTransition(async () => {
removeOptimistic(id); // instant, temporary
await deleteTodo(id); // when this settles, the layer is
}); // dropped and todos re-renders
}
Why it matters, pros and cons
Why it matters
The optimistic state is a layer over server data. Once it settles, the layer goes and the real list shows.
Pros
- Updates feel instant, and the UI stays responsive to new actions in flight.
- A failed request fixes itself: the item reappears with no rollback code.
- No client cache to merge, dedupe or keep in sync with the server.
Cons
- Needs a real server source of truth that revalidates after every action.
- Failures show up late, so say what happened or the item just pops back.
- Skip it for slow, risky or irreversible actions where waiting is honest.