Build1 distinct publisher3 min readPublished
The automatic revert works exactly as documented. The documented example just has nowhere to put a failure, so a rejected server action reads to the user as a tick that quietly unticks a moment later.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
Follow any of these and your For You feed starts watching them — no settings page required.
build
Next.js checks Origin for Server Actions. Move the same logic to a route handler and it stops.1 distinct publisher
build
A strict CSP killed one form in production, and every monitor stayed green1 distinct publisher
build
Next.js renders default.tsx whenever a fresh page load leaves a parallel slot unmatched1 distinct publisher
build
Five frameworks, one store, and a benchmark that measures when the button works1 distinct publisher
The automatic revert is a re-render, not an error signal. That is the whole problem. The optimistic value is dropped once the surrounding transition completes and the component renders again with server data [3]. If the action failed, the `todo` prop never changed, so the render that clears the optimistic value produces the pre-click UI [4]. Pixel for pixel, that is indistinguishable from a mutation that succeeded and returned the same value.
The ordering in the fixed server action shows why. `revalidatePath('/todos')` sits inside the `try`, above the `catch` [6], so the failure branch revalidates nothing [11]. With no revalidation and no new data, the revert lands on the old value. The hook is doing precisely what it says.
So the docs example is correct under an assumption it does not state: that `toggleComplete` cannot fail, or that failure gets surfaced somewhere else. The post's author calls the first version close to the exact React example and notes it quietly assumes success [2]. That assumption holds in a demo, but not in an app where the listed failure modes include a network error, a database error, or an authorization check rejecting the change [4]. Author's framing, which I agree with: showing a false success and then withdrawing it is worse than making the user wait [5].
Note what the fix actually changes. The client-side branch is `if (!result.success)`, which is reachable only because the action catches the exception and returns `{ success: false, message: 'Failed to update' }` [6][12]. An action that throws instead gives the client nothing to branch on at that line. Error semantics move into the action's return type, and the component keeps a `useState` string next to the row it renders [7].
The bulk case is where the arithmetic gets unfriendly. `handleBulkComplete` writes optimistic state for every id, then awaits a single `Promise.all` over the mapped calls [8]. The optimistic writes happen for all five ids, but the outcome is awaited only once. On the rejection path you get zero result objects back, so none of the five items can be named [10]; on the resolve path you get a mixed array that still has to be walked per item [9]. The user sees five ticks, one of which never persisted, and finds out on refresh [9].
What would have to be true for the docs pattern to transfer unmodified: your mutation cannot be rejected, or your failure display lives above the component and can identify which row reverted. If neither holds, the typed-result version is the baseline, and the per-item version is the baseline for anything batched. The interesting part of the post is the batched pattern, and the copy supplied to us cuts off mid-declaration [13].
Ranked by verification strength, evidence, and original report placement.
The post's first TodoItem component calls setOptimisticTodo({ ...todo, completed: !todo.completed }) and then awaits toggleComplete(todo.id), with no code that inspects the result or handles a failure.
The author states this component is close to the exact example from React's own docs, and that it quietly assumes toggleComplete always succeeds and does nothing if it does not.
useOptimistic's mechanism is that the optimistic value automatically reverts once the surrounding transition completes and the component re-renders with real data from the server; the author says this part works as documented.
If toggleComplete fails through a network error, a database error, or an authorization check rejecting the change, the optimistic state briefly shows the toggled value and then reverts, because the underlying todo prop never changed, and the user gets no explanation.
The revised server action wraps Todo.findById and Todo.findByIdAndUpdate in a try block, calls revalidatePath('/todos'), returns { success: true }, and in the catch returns { success: false, message: 'Failed to update' }.
The revised client component adds a useState error string, checks if (!result.success) after awaiting the action, sets 'Could not update. Try again.', and renders that message below the row; the optimistic revert still happens automatically.
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · September 3, 2026
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Checkable on the page, untested off it
Unusually for a lone self-published post, most of what carries weight can be audited by reading: the listings are complete enough to trace where the return value goes, where revalidatePath sits relative to the catch, and what Promise.all leaves the client to work with. What cannot be audited is any runtime assertion — the revert timing rests on the author's paraphrase of React's documented behaviour, and nobody here ran the failing request they tell you to go simulate.
No usage signal of any kind
Nothing in this reporting counts how many codebases copied the docs-shaped component, how often the failure path fires, or whether anyone adopted the suggested fix. The post asks readers to test their own production apps and report back in the comments — an invitation, not a measurement, and we have no comments to read.
Verb outruns the defect
"Lying to users" is a big phrase for an omission in a teaching example, and the headline points at the hook while the body concedes three times that the hook's revert works exactly as documented. The overstatement is modest because the underlying trap is real and reproducible from the code. The genuinely unsupported line is the ranking — that a false tick is worse than a slower, honest one. Plausible; nobody asked a user.
Craft advice with a storefront attached
The last two lines are a Gumroad link and a request to drop findings in the comments — the ordinary economics of a developer publishing under their own byline, where reach converts to template sales. That shapes the packaging more than the content: an alarming title and a check-your-production call to action both travel well. The code advice costs the author nothing to get right and would cost him credibility to get wrong.
Single voice, legible argument
One author, one post, no runtime evidence, no adoption signal and no second publisher to disagree with — that sets a low ceiling. What lifts it off the floor is the kind of claim being made: this is a control-flow argument a reader can check line by line, not a benchmark or a figure that has to be taken on trust. Treat the code reasoning as solid and the user-experience verdict as one developer's opinion.