Handling Forms via Form ActionsEntry 8 of 10
useOptimistic: Instant UI Before the Server Responds
useOptimistic(state, updateFn) returns a temporary copy of state that you can change immediately while an action runs. When the action finishes, it switches back to the real state.
react.devuseOptimistic
Notes
- Signature:
const [optimisticValue, addOptimistic] = useOptimistic(realValue, (current, input) => next).realValue: the source of truth (props, context, or state).updateFn: computes the optimistic value from the current value and whatever you pass toaddOptimistic.
- Flow:
- The user clicks; the action calls
addOptimistic('up')and the UI updates instantly. - The action
awaits the server. - When the action ends, React drops the optimistic value and renders
realValueagain.- Success:
realValuewas updated (e.g. context refreshed), so the UI stays at the new number. - Failure:
realValuedidn't change, so the UI automatically reverts.
- Success:
- The user clicks; the action calls
addOptimisticmust be called inside an action (a form action,formAction, orstartTransition). Called anywhere else, React warns and the update is dropped.- Your note says rollback must be handled manually. React reverts the value automatically; what you must handle is telling the user it failed (an error message).
What / Why / When
- What
Temporary, automatically reverted UI state.
- Why
Votes, likes, and toggles feel instant.
- When
Updates that almost always succeed and where a brief revert on failure is acceptable.
- Use case
Vote counts that change on click.
Code compare
// Opinion.tsximport { use, useOptimistic } from "react";import { OpinionsContext } from "../store/opinions-context";export function Opinion({ opinion: { id, title, body, userName, votes } }: OpinionProps) { const { upvoteOpinion, downvoteOpinion } = use(OpinionsContext); const [optimisticVotes, setVotesOptimistically] = useOptimistic( votes, (prevVotes, mode: "up" | "down") => (mode === "up" ? prevVotes + 1 : prevVotes - 1), ); async function upvoteAction() { setVotesOptimistically("up"); await upvoteOpinion(id); // context updates `votes` when the server confirms } async function downvoteAction() { setVotesOptimistically("down"); await downvoteOpinion(id); } return ( <article> {/* header and body */} <form className="votes"> <button formAction={upvoteAction} aria-label="Upvote"> ▲ </button> <span>{optimisticVotes}</span> <button formAction={downvoteAction} aria-label="Downvote"> ▼ </button> </form> </article> );}use(OpinionsContext) is React 19's alternative to useContext; unlike hooks, it can be called inside if blocks.
Angular has no automatic revert. Update a signal immediately, then restore it if the request fails:
// opinions.store.ts@Injectable({ providedIn: "root" })export class OpinionsStore { private opinionsService = inject(OpinionsService); private _opinions = signal<Opinion[]>([]); readonly opinions = this._opinions.asReadonly(); vote(id: string, mode: "up" | "down") { const previous = this._opinions(); // snapshot const delta = mode === "up" ? 1 : -1; this._opinions.update((list) => // optimistic write list.map((o) => (o.id === id ? { ...o, votes: o.votes + delta } : o)), ); const request = mode === "up" ? this.opinionsService.upvote(id) : this.opinionsService.downvote(id); request.subscribe({ error: () => this._opinions.set(previous), // rollback }); }}// opinion.component.tsexport class OpinionComponent { private store = inject(OpinionsStore); opinion = input.required<Opinion>(); upvote() { this.store.vote(this.opinion().id, "up"); } downvote() { this.store.vote(this.opinion().id, "down"); }}Differences and gotchas
- React: the optimistic value is temporary by design and reverts when the action ends. If the real state never updates (forgot to refresh context), the number jumps back even on success.
- Angular: the optimistic write is permanent until you undo it. Forgetting the
errorrollback leaves the UI wrong. - The same idea appears in TanStack Query (
onMutate+setQueryData+ rollback inonError; TanStack Query, Entry 8).
Quick reference
| React 19 | Angular |
|---|---|
useOptimistic(votes, updateFn) | Signal update in a store |
addOptimistic('up') in an action | _opinions.update(...) |
| Automatic revert when the action ends | Manual set(previous) in error |
| Real value from props/context | Store signal is the real value |