useFormStatus
Reads the pending state and submitted data of the nearest parent form from a child component.
const { pending, data, method, action } = useFormStatus()- Added in React 19.0.0
Return value
{ pending: boolean; data: FormData | null; method: 'get' | 'post'; action: Function | null }A status object describing the parent <form>. pending is true while the parent form is submitting. data is a FormData object holding the submitted values, or null when there is no active submission or no parent form. method is the HTTP method the parent form submits with, defaulting to 'get'. action is the function passed to the parent form's action prop, or null when there is no parent form, when action is a URI string, or when no action prop was given.
Changelog
- PHP 19.0.0Added to react-dom as a stable API alongside function form actions.
useFormStatus is imported from react-dom, not react. It lets a component read the submission state of the <form> above it as though the form were a context provider — no props threaded down, no isSubmitting boolean passed through three layers of markup.
When to reach for it
The intended case is a shared control: a design-system SubmitButton, an inline spinner, a fieldset that dims itself while a save is in flight. Any of these can be dropped into any form and become aware of it automatically. If the pending state is only needed by the component that owns the form, use useActionState's isPending or your own state instead.
What trips people up
- The hook only reads a parent form. Calling it in the same component that renders the
<form>always givespending: false— the reading component must be a child. - It reports nothing for a form rendered by a child component.
- Outside any form it returns defaults rather than erroring, so a typo in component placement fails silently instead of loudly.
pendingonly turns true for function actions; a plain URL submission navigates away instead.
Example
When to use it
- Disable a submit button and swap its label while a save is in flight
- Build a reusable SubmitButton for a design system that works in any form
- Show a spinner next to a fieldset without passing a pending prop down
Frequently asked questions
Why is useFormStatus always returning pending false?
Almost always because the hook is called in the component that renders the <form> rather than in a child of it. Move the call into a child component placed inside the form. It also stays false if the form's action is a URL string instead of a function, since that is a real navigation, not a tracked submission.
Where do I import useFormStatus from?
From react-dom: `import { useFormStatus } from 'react-dom'`. It is a DOM-specific hook, so it does not live in the react package like useState or useTransition, and importing it from react gives you undefined.
What is the difference between useFormStatus and useActionState?
useFormStatus reads the parent form's state from a child without any wiring, which suits shared UI like a submit button. useActionState wraps the action itself in the component that owns the form and additionally gives you the value the action returned, which suits validation errors and results.
More examples
Reading data, method and action, not just pending
Before submitting, the child renders the idle branch, and action is a reference to the upload function passed to the parent form. During the 800ms submission it renders `Uploading notes.txt via GET...`, reading the in-flight FormData without the parent passing anything down. Note what method is: it mirrors the parent form's method prop and defaults to 'get', so it stays 'get' here even though passing a function to action forces the actual HTTP request to POST — write <form method="post" action={upload}> if you want the hook to report 'post'. Once the action resolves, data returns to null and the component falls back to the idle branch.
import { useFormStatus } from "react-dom";
function SubmissionDetails() {
const { pending, data, method, action } = useFormStatus();
if (!pending) {
return <p>Idle. method={method}, action={action ? action.name : "none"}</p>;
}
return (
<p>
Uploading {String(data.get("filename"))} via {method.toUpperCase()}...
</p>
);
}
export default function UploadForm() {
async function upload(formData) {
await new Promise((resolve) => setTimeout(resolve, 800));
console.log("uploaded", formData.get("filename"));
}
return (
<form action={upload}>
<input name="filename" defaultValue="notes.txt" />
<button type="submit">Upload</button>
<SubmissionDetails />
</form>
);
}The mistake: calling the hook in the form's own component
BrokenForm's button never disables: useFormStatus looks for a form above the component that calls it, and the form here is rendered by that same component. FixedForm moves the hook one level down into Submit, which is a child of the <form>, so pending flips to true for the 500ms the action runs. Nothing throws in the broken version — the only symptom is a button that never reacts, which is why this is the most common bug report against this hook.
import { useFormStatus } from "react-dom";
// Broken: pending is always false here.
function BrokenForm() {
const { pending } = useFormStatus();
async function save() {
await new Promise((r) => setTimeout(r, 500));
}
return (
<form action={save}>
<button disabled={pending}>Save</button>
</form>
);
}
// Fixed: the hook lives in a child of the form.
function Submit() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? "Saving..." : "Save"}</button>;
}
export default function FixedForm() {
async function save() {
await new Promise((r) => setTimeout(r, 500));
}
return (
<form action={save}>
<Submit />
</form>
);
}
Discussion