Command Palette

Search for a command to run...

Hectal
PHASE 19Advanced React 19+ ~20 min· topic 6 of 8

Topic 19.6

useOptimistic

In one line

useOptimistic(state, update?) shows a temporary, hoped-for version of some state while an Action runs; when the Action ends React drops the temporary value and shows the real state again, so a failed request reverts by itself.

Think of it like this

Writing a guest's name on a party list in pencil while you wait for their RSVP card. Everyone can already see the name on the list. When the card comes back "yes", you write the name in pen; if it comes back "no", the pencil mark is rubbed out. Optimistic UI is the pencil: show the result you expect right away, then make it permanent or erase it when the real answer arrives.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Optimistic UI
Showing the expected result immediately, before the server confirms it.
Pessimistic UI
Waiting for the server's answer before showing the result, with a loading state meanwhile.
useOptimistic
A hook that shows a temporary value during an Action and drops it when the Action ends.
Passthrough state
The real state you give useOptimistic. It is shown whenever no Action is running.
Revert
Going back to the previous value, here automatically when the Action ends without updating the real state.
Rebase
Re-applying pending optimistic changes on top of newer real state.

Step by step

01Start from real state

Keep the real, confirmed data in state as usual. Then pass it to useOptimistic with a function that says how an optimistic change is applied.

Likes.jsxwhole filejsx
const [likes, setLikes] = useState(10);
const [shownLikes, addOptimistic] = useOptimistic(
  likes,
  (current, delta) => current + delta,
);

02Apply the optimistic change inside an Action

In the Action, call addOptimistic first, then talk to the server. The screen updates at once.

Likes.jsxwhole filejsx
startTransition(async () => {
  addOptimistic(1);              // shown now
  await api.like();              // may fail
  startTransition(() => setLikes((l) => l + 1)); // real state
});

03Success and failure

On success the real state catches up with the optimistic one, and when the Action ends React shows the real state: same number, no flicker. On failure the real state never changed, so dropping the optimistic value is the revert.

Success and failurediagram
Rendering diagram…

04Mark pending items

For lists, add a flag such as sending: true in the update function, and style those items differently. Users then know which items are confirmed.

Try it yourself

  1. 1

    Slow it down

    In the likes example, change the delay to 1500 ms and click "Like (works)" three times quickly. Predict what the number shows during and after. Each pending Action adds 1 on top of the real value.

  2. 2

    Keep the failed text

    In the chat, the form resets even when sending fails, because the action itself didn't throw. Put the failed text back: store it in state and give the input defaultValue={failedText} plus key={failedText}. Predict whether the box shows it after a failure.

  3. 3

    No update function

    Rewrite the likes hook as useOptimistic(likes) and call addOptimistic((n) => n + 1). Does it behave the same?

Code & diagrams

Likes that revert on failure React 19+ New tab

The first like shows 11 at once and stays (the real state caught up). The second shows 12, the server fails, and when the Action ends React drops the optimistic value: back to 11 with no undo code.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

render likes=10 shown=10
render likes=10 shown=11
render likes=11 shown=11
render likes=11 shown=12
failed, reverting
render likes=11 shown=11
An optimistic chat with a form action React 19+ New tab

Each message appears instantly, dimmed. `Hello` is confirmed; `please fail` disappears when its Action ends and the error explains why. Try sending your own messages, including one with the word fail.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

render: Welcome!
render: Welcome!, Hello (sending)
render: Welcome!, Hello
render: Welcome!, Hello, please fail (sending)
render: Welcome!, Hello

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Call addOptimistic outside an Action

Call addOptimistic(1) directly in a button's onClick, without startTransition.

terminal
$ Console
── what you'll see ──
An optimistic state update occurred outside a transition or action. To fix, move the update to an action, or wrap with startTransition.

Break #2

Set the real state with an urgent update

In the chat, replace startTransition(() => setMessages((m) => [...m, saved])) with setMessages((m) => [...m, saved]).

terminal
$ Console
── what you'll see ──
render: Welcome!
render: Welcome!, Hello (sending)
render: Welcome!, Hello, Hello (sending)
render: Welcome!, Hello
...

Myth vs fact

Myth

Optimistic updates need manual rollback code.

Fact

With useOptimistic the rollback is automatic: the optimistic value disappears when the Action ends. You only update real state on success and show an error on failure.

Myth

useOptimistic changes the real state.

Fact

It never touches your state. It shows a temporary value derived from it; you still set the real state yourself.

Myth

Everything should be optimistic.

Fact

Use it for cheap, usually-successful, easily-reversible actions. Payments and destructive actions should wait for confirmation.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Optimistic updates are stored like pending updates on a hook queue and re-applied on top of the latest passthrough value on every render until their transition finishes, which is how they survive other state changes (rebasing).

  • ▸

    Because the optimistic value is tied to the transition, a queue of useActionState dispatches keeps every optimistic item visible until the whole queue drains.

  • ▸

    Use stable keys for optimistic list items (a client-generated id, not the array index) so React keeps their DOM nodes when the confirmed item replaces the optimistic one. crypto.randomUUID() in the action is fine; the server can return the same id.

  • ▸

    Announce failures accessibly: a role="alert" message when an optimistic item is reverted, since the item silently disappearing can confuse screen-reader and sighted users alike.

Remember this

  1. 1

    Optimistic UI means updating the screen before the server confirms, because most requests succeed. Likes, chat messages, ticking a to-do: waiting 300 ms for each feels sluggish. The opposite, pessimistic UI, waits for the answer and shows a spinner. Optimistic is faster to feel, but you must handle failure honestly.

  2. 2

    The hook. const [optimisticState, addOptimistic] = useOptimistic(state, updateFn). state is the real value (usually from useState, props or useActionState). optimisticState equals state whenever no Action is running. Calling addOptimistic(value) during an Action makes React compute updateFn(currentState, value) and show that instead. Without updateFn, addOptimistic(newValue) sets the optimistic value directly (and accepts an updater function like setState).

  3. 3

    When the Action ends, the optimistic value is thrown away. React goes back to showing state. If the Action succeeded, it should have updated state (for example setMessages([...m, saved])), so the screen shows the confirmed version and nothing seems to change. If it failed and didn't update state, the screen goes back to the old value: the revert happens automatically. You don't write undo code; you only show an error message.

  4. 4

    It only works inside an Action. Call addOptimistic inside startTransition, a form action, or an action from useActionState. Outside one React warns An optimistic state update occurred outside a transition or action. and the value is dropped at once. While the Action runs, React also re-applies the optimistic updates on top of any newer real state (it rebases), so other updates don't wipe the pending item.

  5. 5

    Combining with actions. In a form action: read the FormData, call addOptimistic, await the server, then update the real state inside startTransition (Topic 19.2). If you set the real state with a plain urgent update after the await, it commits before the Action ends while the optimistic item is still applied, and the user sees the item twice for a moment. Mark optimistic items (sending: true) so the UI can show them dimmed or with "Sending...": being honest about what isn't saved yet matters for trust and accessibility.

  6. 6

    When not to be optimistic: payments, deleting important data, anything where a reverted result would confuse or harm the user, or requests that fail often. There, show a pending state (Topic 19.5) and wait.

Explain it without notes

01

What is optimistic UI, and when is it a good idea?

02

How does useOptimistic revert a failed update?

03

Why must addOptimistic be called inside an Action?

04

How do you combine useOptimistic with a form action correctly?

Practice

01

Make a to-do checkbox that toggles optimistically and reverts if fakeToggle() rejects.

02

Add optimistic "(sending)" items to a comment form backed by useActionState.

Trade-offs

  • ↔

    Optimistic UI feels instant, but users may act on data that later disappears; mark pending items and keep failures visible.

  • ↔

    Reverting is automatic only for the optimistic value. Side effects you started (navigation, analytics, other state) are not undone for you.

Done when you can

  • I can explain optimistic vs pessimistic UI.

  • I can show an optimistic value with useOptimistic inside an Action.

  • I rely on the automatic revert and show an error on failure.

  • I update real state in startTransition after await to avoid duplicates.

  • I know when not to use optimistic updates.