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 to addOptimistic.
  • Flow:
    1. The user clicks; the action calls addOptimistic('up') and the UI updates instantly.
    2. The action awaits the server.
    3. When the action ends, React drops the optimistic value and renders realValue again.
      • Success: realValue was updated (e.g. context refreshed), so the UI stays at the new number.
      • Failure: realValue didn't change, so the UI automatically reverts.
  • addOptimistic must be called inside an action (a form action, formAction, or startTransition). 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

React
// 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:

Angular
// 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    });  }}
Angular
// 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 error rollback leaves the UI wrong.
  • The same idea appears in TanStack Query (onMutate + setQueryData + rollback in onError; TanStack Query, Entry 8).

Quick reference

React 19Angular
useOptimistic(votes, updateFn)Signal update in a store
addOptimistic('up') in an action_opinions.update(...)
Automatic revert when the action endsManual set(previous) in error
Real value from props/contextStore signal is the real value