React 19 Actions — useActionState · useFormStatus · useOptimistic

[check: …]
📖 guide (.md) ← react deep dive
📖 Pair this live React playground with the companion guide (.md) — this page is the rendered ground truth. ↗ Builds on frontend/react: useState — the evolution of useState for async form handling (pending · result · error in one hook).

useState → Actions: async form state in one hook

Before React 19, a single form submission meant hand-rolling 3-4 pieces: useState for the fields, useState for pending, useState for error, a try/catch/finally around the fetch, and manual disable/enable of the submit button. React 19 collapses that into Actions — async functions passed to <form action> or wrapped with startTransition, plus three hooks that read the resulting lifecycle.

hooksignaturerole
useActionState [state, formAction, isPending] = useActionState(reducerAction, init) "useReducer for side effects" — reducerAction may be async, may POST. Returns the result + a dispatcher + a pending flag.
useFormStatus { pending } = useFormStatus()  (child of <form>) Reads the parent form's pending state from React's internal context — a deep <SubmitButton/> needs zero props.
useOptimistic [optimisticValue, addOptimistic] = useOptimistic(realValue) Layers a predicted value on top during the pending window; React auto-reverts to realValue when the action resolves.

API probe (runtime): useActionState= · useFormStatus= · useOptimistic=. This page simulates the action lifecycle with useReducer (idle→pending→success) and the optimistic overlay with useState — the pattern is identical; the simulation just sidesteps the <form action> React-internal context that is fragile inside an eval sandbox. See the guide for the exact native APIs.

1 · the action state machine (edit me)

2 · Babel compiles JSX → element tree

<ActionsDemo/> becomes React.createElement(ActionsDemo). Each dispatch({{type:'submit'}}) runs the reducer, React re-renders with status:'pending'; the setTimeout then dispatches 'success' and the optimistic overlay is discarded.

// (hit "compile & render" to see Babel's output)

3 · live React (the action lifecycle, proven)

The gold-check drives this automatically: asserts status:"idle" → types a name + clicks Submit → asserts status:"pending" (action started, optimistic value shown) → waits for the simulated async → asserts status:"success" with the committed name. That proves the idle→pending→success state machine and optimistic reconciliation end-to-end.

status: · gold: idle → pending → success + committed name

useActionState vs useReducer

aspectuseReduceruseActionState
reducer purity MUST be pure — no fetch, no timers reducerAction MAY be async & run side effects
return value [state, dispatch] (2 items) [state, dispatchAction, isPending] (3 items)
dispatch contract call anywhere must run inside a startTransition or an Action prop
queuing batched, parallel reducer calls sequential — each call waits for the previous result
built-in pending flag no — you wire isPending yourself yes — third return value
best for UI state machines (todos, steppers) form submissions, mutations, server actions

intent → pattern

intentpatternwhy
submit a form, track pending useActionState(asyncAction, init) + pass dispatchAction to <form action> React wraps the submit in a Transition; isPending is free
show predicted value while waiting useOptimistic(real) + addOptimistic(pred) inside the action instant feedback; React auto-reverts to real on resolve
deep submit button knows it's submitting useFormStatus() in a child of <form> no prop-drilling — reads React's form context
handle validation / server errors return {error} state from reducerAction known errors → UI; unknown throws → nearest Error Boundary