# useFormStatus

## 1. The Problem It Solves

A reusable `<SubmitButton>` component needs to disable itself and show a spinner while the form it lives inside is submitting — without every parent that renders a `<form>` having to manually track a `pending` boolean and pass it down as a prop.

```tsx
// Without useFormStatus: every form's parent must track and pass pending manually
function ContactForm() {
  const [pending, setPending] = useState(false);
  async function action(formData: FormData) {
    setPending(true);
    await submitContact(formData);
    setPending(false);
  }
  return (
    <form action={action}>
      <input name="email" />
      <SubmitButton pending={pending} /> {/* has to be threaded through by hand */}
    </form>
  );
}
```

`useFormStatus` lets `SubmitButton` read its enclosing form's pending status directly, with zero props — the same `<SubmitButton>` works correctly inside `ContactForm`, `SignupForm`, or any other form, automatically tracking whichever form it happens to be rendered inside.

## 2. Signature

```tsx
import { useFormStatus } from "react-dom"; // NOT "react" — this hook lives in react-dom

function SubmitButton() {
  const { pending, data, method, action } = useFormStatus();
  return <button disabled={pending}>{pending ? "Submitting..." : "Submit"}</button>;
}
```

| Field | Type | Meaning |
|---|---|---|
| `pending` | `boolean` | `true` while the enclosing form's action is in flight |
| `data` | `FormData \| null` | the FormData currently being submitted, or `null` when not pending |
| `method` | `"get" \| "post"` | the HTTP method the form is submitting with |
| `action` | `string \| function \| null` | reference to the form's `action` prop |

## 3. Must Be Called in a Child of the `<form>`, Not the Component Rendering It

```tsx
// BROKEN: calling useFormStatus() in the SAME component that renders <form>
function ContactForm() {
  const { pending } = useFormStatus(); // ALWAYS pending: false — no ancestor form exists yet
  return (
    <form action={submitContact}>
      <button disabled={pending}>Submit</button>
    </form>
  );
}
```

```tsx
// FIXED: extract a child component rendered INSIDE the <form>
function SubmitButton() {
  const { pending } = useFormStatus(); // correctly reads the ancestor <form>'s live status
  return <button disabled={pending}>Submit</button>;
}

function ContactForm() {
  return (
    <form action={submitContact}>
      <input name="email" />
      <SubmitButton />
    </form>
  );
}
```

The rule: `useFormStatus` reports the status of the nearest **ancestor** `<form>`. The component that renders the `<form>` tag is not inside its own form's subtree — the form is its descendant, not its ancestor — so calling the hook there always returns the inactive default.

## 4. Composed with useActionState

`useFormStatus` and `useActionState` (previous topic) are commonly used together: `useActionState` manages the form's submission result and its own `pending` flag at the call site that owns the action, while `useFormStatus` lets any nested component independently read that same pending status without the action's owner passing it down manually.

```tsx
import { useActionState } from "react";
import { useFormStatus } from "react-dom";

async function submitContact(_prevState: string | null, formData: FormData) {
  await new Promise((resolve) => setTimeout(resolve, 1000)); // simulated network delay
  return `Thanks, ${formData.get("email")}!`;
}

function SubmitButton() {
  const { pending } = useFormStatus(); // independent read — no prop from ContactForm needed
  return <button disabled={pending}>{pending ? "Sending..." : "Send"}</button>;
}

function ContactForm() {
  const [message, formAction] = useActionState(submitContact, null);
  return (
    <form action={formAction}>
      <input name="email" />
      <SubmitButton />
      {message && <p>{message}</p>}
    </form>
  );
}
```

`SubmitButton` never receives `pending` as a prop — it independently subscribes to the enclosing `<form>`'s status via `useFormStatus`, and React keeps both `useActionState`'s pending flag and `useFormStatus`'s pending flag in sync with the same underlying form-action transition.

## 5. Reading `data`, `method`, and `action`

```tsx
function SubmitButton() {
  const { pending, data } = useFormStatus();
  const submittingEmail = data?.get("email");
  return (
    <button disabled={pending}>
      {pending ? `Submitting: ${submittingEmail}...` : "Submit"}
    </button>
  );
}
```

Reading `data` directly off `useFormStatus()` lets a child component reflect exactly what's in flight — no need to duplicate that value into separate state just so a nested component can display it.

![A SubmitButton fiber reads pending status from its ancestor form fiber; the component that renders the form itself has no ancestor form to read from](diagrams/01-useformstatus-ancestor.svg)

## 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 `this` and no memory of its own. Hook data has to live somewhere else: it lives on the **fiber**, React's internal object representing one component instance in the tree.

Each fiber holds a `memoizedState` pointer to the head of a **singly linked list of hook objects**, one node per hook call, built in the exact order those hooks are called. Every render, React walks this list from the head, one node per hook call, in lockstep with your function body executing top to bottom. Matching is done purely by **call order** — not by variable name, not by any key. This is the entire reason the Rules of Hooks forbid conditional hook calls: wrap a hook in an `if` and, on some render, every hook call after it shifts one slot in the walk — silently reading and writing the wrong hook's stored data.

`useFormStatus` is unusual among the hooks in this curriculum: its hook node doesn't hold state that its own component produced. Instead, calling `useFormStatus()` walks up the fiber tree looking for the nearest ancestor `<form>` fiber and reads the pending action/transition status React DOM tracks on that form fiber — the same underlying pending-transition tracking that powers `useActionState`'s `pending` flag for the action bound to that form. This is conceptually parallel to how `useContext` walks up to the nearest Provider fiber, though it is a dedicated React DOM mechanism specific to `<form>` elements and their `action` prop, not the general Context mechanism — there is no explicit `<FormStatus.Provider>` anywhere in your code; the `<form>` element itself is the implicit source React DOM tracks.

This walk-up-to-an-ancestor mechanism is exactly why calling `useFormStatus()` in the same component that renders the `<form>` tag doesn't work as one might hope: at the point that component's own function body is executing, it is the one about to render the `<form>` element — the form doesn't exist as a fiber yet, and even once it does, it's a *descendant* of that component, not an ancestor. The upward walk finds nothing and the hook returns its inactive default (`pending: false`, `data: null`). A genuinely separate child component, rendered inside the `<form>` tag's JSX, has that `<form>` fiber sitting above it in the tree once mounted, and the upward walk finds it and subscribes that child to re-render whenever the form's pending status changes.

## Common Pitfalls

1. **Calling `useFormStatus` in the same component that renders the `<form>`** — always returns the inactive default; extract a child component and call the hook there instead.
2. **Importing from `"react"` instead of `"react-dom"`** — `useFormStatus` is a React DOM hook, not a core React hook.
3. **Assuming it tracks any nearby form** — it specifically reports the *nearest ancestor* `<form>`; separate sibling forms each need their own descendant component independently calling the hook.
4. **Confusing `pending` with general async loading state** — it reflects only the enclosing form's action transition, not arbitrary async work happening elsewhere in the app.
5. **Expecting `data` to be populated when not pending** — `data` is `null` outside of an in-flight submission; guard with optional chaining (`data?.get(...)`).

## Quick Reference

```
import { useFormStatus } from "react-dom";     → note: react-dom, not react
const { pending, data, method, action } = useFormStatus();
pending  → boolean, true while the ancestor form's action is in flight
data     → FormData | null, the in-flight submission's data
method   → "get" | "post"
action   → the form's action reference

Rule: must be called in a CHILD of the <form>, never the component rendering the <form> itself.
```
