Next.js

Verified against Claude Code · 2026-08-02

Add optimistic UI to a Server Action mutation without letting the UI lie on failure

Wires useOptimistic to a Server Action so a mutation appears instantly with a real temporary identity, and defines exactly what happens to that optimistic state when the server rejects it.

Claude CodeCursorv0 by Vercel4 fillable variables

The prompt

Ready to copy — highlighted parts are example details you can swap.

You are adding optimistic UI to a specific mutation that already runs through a Server Action, using useOptimistic to show the result before the server has actually confirmed it — and you are defining, explicitly, what happens to that optimistic state if the mutation fails, rather than leaving the failure path unhandled.

MUTATION
Adds a comment to a task and should appear in the comment list immediately as the user submits it

CURRENT SERVER ACTION
addCommentAction(taskId, formData), which validates the comment text, inserts a row, and returns { comment } on success or { error } on failure

WHY OPTIMISTIC UI IS NEEDED HERE
Comments are posted frequently during active discussions, and waiting even a second for each one to appear makes the thread feel unresponsive compared to how instant it feels elsewhere in the app

FAILURE BEHAVIOR
The optimistic comment should disappear from the list and a small inline message should say 'Failed to post — try again' next to where the comment box is, with the typed text restored into the box

BUILD RULES
Confirm first that this mutation actually warrants useOptimistic before building anything: the interface has to render a visible result — a new item appearing in a list, a count changing, a toggled state flipping — before the server has confirmed the mutation succeeded, for the added complexity to earn its place; if the UI would be showing a pending spinner either way and nothing changes about what's rendered once the optimistic value lands, useOptimistic isn't buying anything here and a plain pending state is the correct, simpler choice. When constructing the optimistic value inside the updater function passed to useOptimistic, give it a genuine temporary identity — a locally generated id, clearly distinguishable from a real server-assigned one, such as a prefixed string or a negative placeholder number — so the optimistic item can be found and reconciled or removed later without accidentally colliding with a real id a concurrent request might assign. Reconcile the optimistic state with the actual server response the moment the Server Action resolves successfully: replace the temporary item with the real one returned by the action, including its real id and any server-computed fields the optimistic guess couldn't have known, rather than leaving the temporary version rendered indefinitely alongside or instead of the confirmed one. Define the failure path explicitly and make it visible to the user, not silent: when the Server Action's returned state indicates failure, the optimistic value must be rolled back — removed from the list, or reverted to its prior value — and the user needs a clear, specific signal that their action didn't actually take effect, not just a state that quietly reverts with no explanation, which reads as a bug rather than a handled failure. Keep the optimistic update itself free of business logic the server is responsible for — don't have the client-side optimistic guess attempt to validate the input or compute a value only the server can correctly determine, such as a price that depends on tax rules; the optimistic value should be the client's best guess at what the confirmed result will look like, not a parallel reimplementation of server logic that can disagree with the real computation.

OUTPUT FORMAT
The Server Action's return type, the component using useOptimistic including the updater function, and one paragraph specifically describing what a user sees, step by step, if the mutation fails after the optimistic value has already rendered.

Customize

Optional — swap in your own details for the highlighted parts above.

Why this works

Requiring a justification for useOptimistic before building anything targets the same overcorrection this hook invites everywhere it's discussed as a React 19 highlight: it's genuinely useful specifically when the interface renders a different, visible result before versus after the optimistic value lands, and completely wasted complexity when the UI would show identical content in both cases, such as a form that just disables its submit button either way — a build that skips the justification step tends to add useOptimistic reflexively because it's the newer API, which produces real indirection (a second, temporary state to reason about, reconcile, and roll back) for a mutation that never needed it. Giving the optimistic value a genuine, distinguishable temporary identity matters because two things can legitimately be true at once during the pending window — the optimistic item is rendering locally, and a real server-assigned id doesn't exist yet — and if the optimistic id isn't clearly marked as temporary, reconciling it with the eventual real response becomes ambiguous the moment more than one mutation is in flight at once, since there's no reliable way to tell which rendered item corresponds to which pending request without a marker that says so explicitly. The mandatory, visible failure path is the part a first pass at useOptimistic most commonly skips, precisely because the sunny-day case — mutation succeeds, optimistic value gets confirmed — is what every basic example demonstrates, and a hook that silently reverts state on failure with no accompanying message produces a genuinely confusing experience: a comment appears, then vanishes a moment later with no explanation, which reads to the person who just typed it as the app randomly deleting their input rather than as a failed request being handled correctly. Keeping business logic out of the optimistic guess — not letting the client-side update attempt to compute something only the server can correctly determine — prevents a specific and subtle class of bug where the optimistic value and the eventual real value disagree not because of network timing but because the client's guess used different logic than the server's actual computation, which shows up as a visible, jarring correction the moment the real value replaces the optimistic one, even when the mutation itself succeeded exactly as intended.

What you get back

const [optimisticComments, addOptimisticComment] = useOptimistic(comments, (state, newComment) => [...state, { ...newComment, id: 'temp-' + Date.now(), pending: true }]);

async function handleSubmit(formData) { const text = formData.get('text'); addOptimisticComment({ text, author: currentUser }); const result = await addCommentAction(taskId, formData); if (result.error) { // optimistic entry is removed on next render since it's not in the real 'comments' prop; // show inline "Failed to post — try again" and restore the typed text into the input } }

Failure path: the user submits a comment, sees it appear instantly with a subtle pending indicator, and if addCommentAction returns an error, that comment disappears from the list on the next render (since the confirmed comments array never included it) while an inline message appears where the comment box is, and the originally typed text is restored into the box rather than lost.

Verified against

Claude Code Sonnet 4.6 · 2026-08-02

v0 by Vercel 2026.7 · 2026-08-08

Changelog

  • 2026-08-02 — Initial publish, verified against Claude Code (Sonnet 4.6) and v0 by Vercel on a Next.js 16 comment thread using useOptimistic with a Server Action.

Need this built into your business?

If a prompt isn't enough — custom software, built and maintained for you — that's Scult's day job.

EXPLORE CUSTOM SOFTWARE
All Next.js prompts

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY