# useActionState

## 1. The Problem It Solves

Handling a form submission that hits the network traditionally means wiring up `pending`, `error`, and `result` state by hand, and manually calling `preventDefault` before doing async work:

```tsx
// Without useActionState: three separate pieces of state, wired by hand
function ContactForm() {
  const [pending, setPending] = useState(false);
  const [message, setMessage] = useState<string | null>(null);

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setPending(true);
    const formData = new FormData(e.currentTarget);
    const result = await submitContact(formData);
    setMessage(result);
    setPending(false);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" />
      <button disabled={pending}>{pending ? "Sending..." : "Send"}</button>
      {message && <p>{message}</p>}
    </form>
  );
}
```

`useActionState` bundles "the latest result of an async action" and "whether it's currently in flight" into a single hook, and hands back a form-compatible `action` function that plugs directly into `<form action={...}>` — no manual `preventDefault`, no manual `pending` state.

## 2. The Basic API

```tsx
import { useActionState } from "react";

async function submitContact(previousState: string | null, formData: FormData) {
  await new Promise((resolve) => setTimeout(resolve, 1000)); // simulated network call
  const email = formData.get("email") as string;
  return `Thanks, we'll email you at ${email}!`;
}

function ContactForm() {
  const [message, formAction, isPending] = useActionState(submitContact, null);

  return (
    <form action={formAction}>
      <input name="email" type="email" />
      <button disabled={isPending}>{isPending ? "Sending..." : "Send"}</button>
      {message && <p>{message}</p>}
    </form>
  );
}
```

`useActionState(actionFn, initialState)` returns a 3-tuple: `[state, formAction, isPending]`.

- `state` — whatever `actionFn` most recently returned (or `initialState` before the first submission).
- `formAction` — a wrapped version of `actionFn` to pass to `<form action={...}>` (or call directly).
- `isPending` — `true` while the action is in flight.

Crucially, `actionFn` itself takes the **previous state as its first argument**, followed by the form data — that's the shape React requires so it can thread `state` from one submission to the next.

## 3. Why the Action Signature Takes `previousState` First

```tsx
// The action's signature is ALWAYS (previousState, formData) => newState
async function voteAction(previousState: number, formData: FormData): Promise<number> {
  const delta = formData.get("direction") === "up" ? 1 : -1;
  return previousState + delta; // builds on the last committed result
}

function VoteWidget() {
  const [votes, formAction] = useActionState(voteAction, 0);
  return (
    <form action={formAction}>
      <button name="direction" value="up">Upvote ({votes})</button>
    </form>
  );
}
```

This mirrors `useReducer`'s `(state, action) => newState` shape deliberately — `useActionState` is conceptually "a reducer whose action is a form submission and whose reducer function is allowed to be async." Each submission's result becomes the input to the *next* submission's `previousState`, letting an action accumulate — a running total, a growing list of errors, an incrementing counter — without any extra state variables.

## 4. Handling Validation Errors Without Extra State

Because the action's return value *is* the hook's state, validation/error results flow through the same channel as success results — no separate `error` state needed:

```tsx
interface FormResult {
  success: boolean;
  message: string;
}

async function submitEmail(_prev: FormResult | null, formData: FormData): Promise<FormResult> {
  const email = formData.get("email") as string;
  if (!email.includes("@")) {
    return { success: false, message: "Please enter a valid email." };
  }
  await sendEmail(email);
  return { success: true, message: "Sent!" };
}

function EmailForm() {
  const [result, formAction, isPending] = useActionState(submitEmail, null);
  return (
    <form action={formAction}>
      <input name="email" />
      <button disabled={isPending}>Submit</button>
      {result && (
        <p style={{ color: result.success ? "green" : "red" }}>{result.message}</p>
      )}
    </form>
  );
}
```

## 5. Calling the Action Outside a `<form>`

`formAction` isn't restricted to `<form action={...}>` — it can be called directly (e.g. from a button's `onClick`, bound with extra arguments), which is useful when the "form" is really just a single button triggering a server mutation:

```tsx
async function deleteItemAction(_prev: string | null, itemId: string) {
  await deleteItem(itemId);
  return `Deleted item ${itemId}`;
}

function DeleteButton({ itemId }: { itemId: string }) {
  const [status, deleteAction, isPending] = useActionState(deleteItemAction, null);
  return (
    <button onClick={() => deleteAction(itemId)} disabled={isPending}>
      {isPending ? "Deleting..." : "Delete"}
    </button>
  );
}
```

Note this variant's action takes whatever argument the caller passes as its second parameter — it need not be a `FormData` at all; `FormData` only appears automatically when `formAction` is wired to a real `<form action={...}>`.

## 6. Composed With useFormStatus

A component nested inside the `<form>` can read the same in-flight status independently via `useFormStatus` (from `react-dom`), without `isPending` being threaded down as a prop — see the useFormStatus topic for the full picture of how the two hooks share the same underlying pending-transition tracking.

![useActionState wraps an async action; dispatching a submission enqueues a transition that updates state and isPending together once the action settles](diagrams/01-useactionstate-flow.svg)

## 7. Under the Hood

React does not attach state to the component *function* — a function component is just a function, called fresh every render with no persistent memory of its own. Hook data lives on the **fiber**, in the same **singly linked list of hook objects** every other hook in this curriculum uses, walked in strict call order every render — which is why `useActionState`, like all hooks, cannot be called conditionally.

`useActionState`'s hook node holds the current `state` value, much like a `useState`/`useReducer` node does. But dispatching the wrapped `formAction` doesn't just enqueue a plain update — React wraps the call in a **transition**, the same lower-priority scheduling mechanism that powers `useTransition` and Suspense-driven navigation. Concretely: calling `formAction` marks the pending action as a transition, flips this hook's `isPending` flag to `true` for the *current* render pass, invokes your action function (`await`-ing it if it's async), and — once the returned promise settles — schedules an update that writes the action's return value into this hook's `state` slot and flips `isPending` back to `false`. Because it's a transition, React can keep rendering the existing UI responsively while the action is in flight, rather than blocking on it.

This is also why the action's signature takes `previousState` first: React always calls your action function with whatever is currently sitting in this hook's `state` slot as the first argument, then commits the return value as the new `state` once it resolves — structurally identical to how `useReducer` always calls the reducer with the current `state` and an action payload, except here the "reducer" is your (possibly async) action function and the "action payload" is the form data (or whatever argument you pass when calling it directly).

`useFormStatus`, covered in the previous topic, is not a separate mechanism from this — it reads the *same* underlying pending-transition status React DOM associates with the ancestor `<form>`'s action, just from a different fiber (a descendant, via an upward tree walk) rather than from `useActionState`'s own hook node on the fiber that called it.

## 8. Common Pitfalls

1. **Forgetting the action's first parameter is `previousState`, not the form data** — the signature is always `(previousState, formData) => newState`; putting form data first silently breaks the state-threading contract.
2. **Mutating `previousState` in place instead of returning a new value** — exactly like `useReducer`, the action should be treated as pure with respect to its inputs, returning a new state value rather than mutating the old one.
3. **Manually managing a separate `pending`/`error` state alongside `useActionState`** — defeats the purpose; let the action's return value carry error/result info, and read `isPending` directly from the hook.
4. **Calling `formAction` and expecting a return value synchronously** — the action runs as a transition; its result lands in `state` on a subsequent render, not as this call's return value.
5. **Assuming `useActionState` requires a real `<form>`** — `formAction` can be called directly (e.g., from `onClick`) with any argument shape; the `FormData` convention only applies when wired to `<form action={...}>`.

## 9. Quick Reference

```
const [state, formAction, isPending] = useActionState(actionFn, initialState)

actionFn: (previousState, formData) => newState   → newState may be a Promise
formAction  → pass to <form action={formAction}> OR call directly with any arg
isPending   → true while the action's transition is in flight

Error handling: return an error shape from actionFn instead of throwing;
                no separate error state needed — it's just part of `state`.
```
