QuestionsReact

React 19 Actions & form hooks

React 19 / ActionsHardReact

What are Actions in React 19? Explain useActionState, useFormStatus, useOptimistic, and the use() hook, and the problem they collectively solve.

What it tests

Whether you know React 19's built-in async/form primitives that replace hand-rolled pending/error/optimistic plumbing.

Approach & answer

The problem: for years, submitting a form meant hand-wiring the same boilerplate — an isPending state, a try/catch to capture errors, a manual reset, and often an optimistic update with manual rollback. React 19 folds this into 'Actions': an async function you hand to React (via a form's action prop or a transition) that React manages, automatically tracking pending state, errors, and sequencing. Four primitives sit on top. (1) useActionState(fn, initialState) returns [state, dispatch, isPending]: you pass an async action that receives the previous state (and FormData when used as a form action) and returns the next state; React gives you isPending for free and serializes concurrent submissions. (2) useFormStatus() reads the pending status of the nearest ANCESTOR form without any prop-drilling — so a deeply nested SubmitButton can disable itself while the parent form submits, which is the whole reason it exists. (3) useOptimistic(actualValue, updateFn) returns an optimistic value you can set instantly on submit; React shows it immediately and AUTOMATICALLY reverts to the real value when the action settles — no manual rollback. (4) use(promise) unwraps a promise (or context) DURING render: it suspends until the promise resolves and integrates with Suspense; unlike hooks, use() may be called conditionally and inside loops. Together they compose with Server Components and server actions: a <form action={serverAction}> works with progressive enhancement, and the client hooks layer pending/optimistic UI on top. The mental shift is declarative async: you describe the action and the optimistic result, and React owns the pending/error/reset lifecycle.

Use this technique when

Form submission with pending/error handling, optimistic UI without manual rollback, or reading form-pending state in a nested button.

Code

function NameForm() {
  const [error, submitAction, isPending] = React.useActionState(
    async (prevState, formData) => {
      const res = await save(formData.get('name'));   // the Action
      if (!res.ok) return 'Could not save';            // becomes the next state
      return null;
    },
    null                                               // initial state
  );
  return (
    <form action={submitAction}>
      <input name="name" />
      <SubmitButton />
      {error && <p role="alert">{error}</p>}
      {isPending && <p>Saving…</p>}
    </form>
  );
}

// Reads the PARENT <form>'s pending state — no props threaded down.
function SubmitButton() {
  const { pending } = ReactDOM.useFormStatus();
  return <button disabled={pending}>{pending ? 'Saving…' : 'Save'}</button>;
}

References