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-domhook 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
nullwhen 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.
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.
<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.
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
Disable the whole form
Write a
FormFieldscomponent that wraps the inputs in<fieldset disabled={pending}>usinguseFormStatus. Predict: can you type while it submits? - 2
Add a spinner
In the second example, add a
Spinnercomponent inside each form that returns<span role="status">Working...</span>only whilepending. Which spinner appears when the newsletter submits?
Code & diagrams
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
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=nullThe 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
Console
Nothing logged yet.Expected console output
during: Search | Subscribing me@example.com (disabled)
after: Search | SubscribeBreak 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.
Break #2
Read data when not pending
In SubmitButton, render {data.get("q")} always, not only while pending.
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),
useFormStatusworks the same: the pending state covers the network round trip, anddatalets the UI show what's being sent before the server answers.
Remember this
- 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 passisPendingdown as a prop through every layer (prop drilling, Topic 9.1) or put it in context yourself.useFormStatusreads it directly, like a built-in context that every<form>provides. - 2
What it returns.
const { pending, data, method, action } = useFormStatus().pendingistruewhile the parent form's action is running.datais theFormDatabeing submitted (ornullwhen not submitting), handy for showing "Sending 'hooks'...".methodis the form's method ("get"or"post"; function actions report"get"unless you setmethod), andactionis the function (or URL) being run, ornull. Import it from **react-dom**, notreact: it's tied to the DOM<form>element. - 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, sopendingis alwaysfalse. Move the button into its own component (that's the whole design-system pattern). - 4
Rule 2: it only sees its own form. Two forms on a page each have their own status; a
SubmitButtonin the search form doesn't go pending when the newsletter form submits. A component outside any form always getspending: falseanddata: 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
useFormStatus or useActionState? Use
useActionState'sisPendingin the component that owns the action and needs its result. UseuseFormStatusin 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 theformActionfromuseActionState, and the button inside readsuseFormStatus. - 6
Accessibility. Disabling the button while pending stops double submits. Also tell assistive technology something is happening:
aria-busyon the button or arole="status"live region with "Saving..." (Phase 18). Keep the button's label meaningful while disabled ("Saving...", not an empty spinner).
Explain it without notes
What problem does useFormStatus solve?
Why does useFormStatus return pending: false in the component that renders the form?
When would you use useFormStatus and when useActionState's isPending?
Practice
Write a SubmitButton that shows Saving "<title>"... using the submitted title field while pending.
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
SubmitButtonmoved outside its form silently stops working. Name and document such components clearly. - ↔
useFormStatusonly covers form actions; for non-form Actions useuseTransitionoruseActionStateand 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,methodandaction.I know when to use
useFormStatusvsuseActionState.I make pending states accessible with
aria-busyor a status region.