Command Palette

Search for a command to run...

Hectal
PHASE 19Advanced React 19+ ~19 min· topic 5 of 8

Topic 19.5

useFormStatus

In one line

useFormStatus() from react-dom lets any component rendered inside a <form> read that form's submission status (pending, data, method, action) without props, which is how design-system submit buttons show their own loading state.

Think of it like this

The "Now serving" light above a bank counter. Anyone standing in that counter's queue can look up and see whether their counter is busy; nobody has to walk over and tell them. People in another counter's queue see their own light, not this one. useFormStatus is the light: any component inside a form can look up and see if its form is submitting.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

useFormStatus
A react-dom hook that tells a component inside a form whether that form is submitting.
pending
True while the parent form's action is running.
data
The FormData being submitted, or null when the form is not submitting.
Design system
A team's shared set of UI components (buttons, inputs, dialogs) used across the app.
Parent form
The nearest <form> above a component in the tree.
aria-busy
An attribute telling screen readers that an element is being updated or is working.

Step by step

01Move the button into its own component

The hook must run in a component the form renders. So the submit button becomes a small component, used inside the form.

SubmitButton.jsxwhole filejsx
import { useFormStatus } from "react-dom";

export function SubmitButton({ children }) {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending} aria-busy={pending}>
      {pending ? "Saving..." : children}
    </button>
  );
}

02Use it in any form

Every form with a function action now gets a loading button for free. No props carry the pending state.

Profile.jsxwhole filejsx
<form action={saveProfile}>
  <input name="bio" />
  <SubmitButton>Save profile</SubmitButton>
</form>

03Each form has its own status

The hook looks up the tree to the nearest <form>. Buttons in other forms, and components outside forms, don't see this submission.

Each form has its own statusdiagram
Rendering diagram…

04Use data for a better message

While pending, data is the FormData being sent, so the button can say what it is doing: Searching "hooks".... When not pending, data is null, so read it only while pending is true.

Try it yourself

  1. 1

    Disable the whole form

    Write a FormFields component that wraps the inputs in <fieldset disabled={pending}> using useFormStatus. Predict: can you type while it submits?

  2. 2

    Add a spinner

    In the second example, add a Spinner component inside each form that returns <span role="status">Working...</span> only while pending. Which spinner appears when the newsletter submits?

Code & diagrams

What useFormStatus reports, inside and outside the form React 19+ New tab

Only `Submit`, which is inside the form, re-renders with the status. While pending it sees the FormData, the method and the action function itself. `Outside` never changes.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

Outside: pending=false
Submit: pending=false data=null method=null action=null
Submit: pending=true data=react method=get action=search
Submit: pending=false data=null method=null action=null
One design-system button, two forms React 19+ New tab

The same component in two forms: only the newsletter button goes pending. No props carry the status. Press Search yourself to see the other one.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

during: Search | Subscribing me@example.com (disabled)
after: Search | Subscribe

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Call it in the component that renders the form

In a ContactForm component, call const { pending } = useFormStatus() and render <form action={fakeSend}>…<button disabled={pending}> from the same component, then submit.

terminal
$ Console
── what you'll see ──
ContactForm pending=false
button: Send

Break #2

Read data when not pending

In SubmitButton, render {data.get("q")} always, not only while pending.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Cannot read properties of null (reading 'get')
An error occurred in the <SubmitButton> component.

Myth vs fact

Myth

useFormStatus comes from react.

Fact

It's exported from react-dom, because it's tied to the DOM <form> element.

Myth

It tells you the status of any form on the page.

Fact

Only the nearest parent form of the component that calls it.

Myth

It replaces useActionState.

Fact

It only reports that a submit is happening. useActionState keeps the action's result. Most real forms use both.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Conceptually every <form> with a function action acts like a context provider for its status. React updates that status inside the same transition as the action, so the button's pending render and the action's result commit consistently.

  • ▸

    Because the status lives on the form, deeply nested design-system pieces (a toolbar inside a fieldset inside a card) can all react to the same submit without coordinating, and without re-rendering components that don't call the hook.

  • ▸

    With Server Functions (Phase 20), useFormStatus works the same: the pending state covers the network round trip, and data lets the UI show what's being sent before the server answers.

Remember this

  1. 1

    The problem it solves. A submit button usually lives deep inside a form, often as a shared <SubmitButton> from your design system (the shared set of UI components a team reuses). To show "Saving..." it needs the form's pending state. Without this hook you'd pass isPending down as a prop through every layer (prop drilling, Topic 9.1) or put it in context yourself. useFormStatus reads it directly, like a built-in context that every <form> provides.

  2. 2

    What it returns. const { pending, data, method, action } = useFormStatus(). pending is true while the parent form's action is running. data is the FormData being submitted (or null when not submitting), handy for showing "Sending 'hooks'...". method is the form's method ("get" or "post"; function actions report "get" unless you set method), and action is the function (or URL) being run, or null. Import it from **react-dom**, not react: it's tied to the DOM <form> element.

  3. 3

    **Rule 1: call it in a component rendered *inside* the form.** It reads the status of the nearest parent <form> in the component tree. If you call it in the same component that renders the <form>, that form is not its parent, so pending is always false. Move the button into its own component (that's the whole design-system pattern).

  4. 4

    Rule 2: it only sees its own form. Two forms on a page each have their own status; a SubmitButton in the search form doesn't go pending when the newsletter form submits. A component outside any form always gets pending: false and data: null. Rule 3: it's a hook, so the usual hook rules apply (top level of a component or custom hook, Topic 4.3).

  5. 5

    useFormStatus or useActionState? Use useActionState's isPending in the component that owns the action and needs its result. Use useFormStatus in child components that only need to know a submit is happening: buttons, spinners, disabling a fieldset. They work together: the form's action can be the formAction from useActionState, and the button inside reads useFormStatus.

  6. 6

    Accessibility. Disabling the button while pending stops double submits. Also tell assistive technology something is happening: aria-busy on the button or a role="status" live region with "Saving..." (Phase 18). Keep the button's label meaningful while disabled ("Saving...", not an empty spinner).

Explain it without notes

01

What problem does useFormStatus solve?

02

Why does useFormStatus return pending: false in the component that renders the form?

03

When would you use useFormStatus and when useActionState's isPending?

Practice

01

Write a SubmitButton that shows Saving "<title>"... using the submitted title field while pending.

02

Write a PendingNotice component that renders a role="status" paragraph Sending... only while its form submits.

Trade-offs

  • ↔

    Reading status implicitly from the parent form removes prop drilling but hides a dependency: a SubmitButton moved outside its form silently stops working. Name and document such components clearly.

  • ↔

    useFormStatus only covers form actions; for non-form Actions use useTransition or useActionState and pass the flag yourself.

Done when you can

  • I can build a reusable submit button with useFormStatus.

  • I know the hook must be called inside a component rendered within the form.

  • I can read pending, data, method and action.

  • I know when to use useFormStatus vs useActionState.

  • I make pending states accessible with aria-busy or a status region.