useFormStatus()
Reads the submission state of the enclosing form from a component rendered inside it.
const { pending, data, method, action } = useFormStatus()- Added in Next.js v14.0.0
Return value
{ pending: true; data: FormData; method: 'get' | 'post'; action: ((formData: FormData) => void | Promise<void>) | string | null } | { pending: false; data: null; method: null; action: null }A status object for the nearest parent form. While that form is submitting, pending is true, data is the FormData being submitted, method is 'get' or 'post', and action is the function passed to the form's action prop — null when that prop is a URI string or absent. Outside an active submission, and when there is no parent form at all, the hook returns the idle shape: pending is false and data, method and action are all null.
Changelog
- PHP v15.0.0React 19 adds data, method and action to the returned object. On earlier React versions only pending is available.
useFormStatus lets a reusable submit button know whether its form is busy without the form having to pass a prop down. Import it from react-dom, not react. Reach for it when the button lives in a shared component — a design-system <SubmitButton> used by a dozen forms — or when the form itself is a Server Component and cannot hold hook state.
The placement rule
The hook only reads a parent form. Call it in the component that renders <form> and it reports pending: false forever, because the form it would report on is a sibling in the same render, not an ancestor. Extract the button into its own 'use client' component and nest it inside the form. This is the single most common bug report against the hook, and it fails silently rather than warning.
What else to know
Only one status is available: the nearest parent form. Nested forms are not legal HTML, so there is no ambiguity, but a status read outside any form is the idle shape rather than an error. If the component already calls useActionState, use that hook's isPending instead — reaching for both is redundant.
Example
When to use it
- Disable a shared submit button and swap its label while the action runs.
- Render a spinner beside a file input using the FormData being uploaded.
- Grey out a whole fieldset during submission from a nested wrapper component.
Frequently asked questions
Why is pending always false from useFormStatus?
The hook is being called in the component that renders the form rather than in a component nested inside it. It only reports on a parent form, so move the button into its own component and render it as a child of `<form>`.
Do I import useFormStatus from react or react-dom?
From `react-dom` — it is a DOM-specific hook, unlike useActionState and useOptimistic which come from `react`. Importing it from `react` gives an undefined export at runtime.
Can useFormStatus track a form rendered by a child component?
No — it walks up to the nearest parent form only, so a form rendered below the calling component is invisible to it. Status flows down the tree, never up.
More examples
The correct nesting
`SubmitButton` is the only client module in the tree, so the form and its inputs stay server-rendered. Because the button is a descendant of `<form>`, it sees the parent's pending state and can be dropped into any other form with no wiring.
// app/ui/submit-button.tsx
'use client'
import { useFormStatus } from 'react-dom'
export function SubmitButton({ label }: { label: string }) {
const { pending } = useFormStatus()
return (
<button disabled={pending} type="submit">
{pending ? 'Saving…' : label}
</button>
)
}
// app/ui/signup.tsx — stays a Server Component
import { SubmitButton } from './submit-button'
import { createUser } from '@/app/actions'
export function Signup() {
return (
<form action={createUser}>
<input type="text" name="email" required />
<SubmitButton label="Sign up" />
</form>
)
}The placement that silently fails
Nothing throws and nothing is logged — the button simply never disables, so double submissions get through. Either extract the button into a child component or switch this component to `useActionState`, which reports pending from the same level that renders the form.
'use client'
import { useFormStatus } from 'react-dom'
import { createUser } from '@/app/actions'
export function Signup() {
// Wrong: this reads a PARENT form. The <form> below is a sibling
// in this render, so `pending` stays false forever.
const { pending } = useFormStatus()
return (
<form action={createUser}>
<input type="text" name="email" required />
<button disabled={pending}>Sign up</button>
</form>
)
}Using data and method from the status
`data` is the live FormData for the in-flight submission, so the status line can name the exact file without lifting state out of the input. Guard on `pending` first: outside a submission the hook returns the idle shape, where `data`, `method` and `action` are all `null` — `method` does not fall back to the form's own attribute — so reading `method.toUpperCase()` without the guard would throw.
'use client'
import { useFormStatus } from 'react-dom'
export function UploadProgress() {
const { pending, data, method } = useFormStatus()
if (!pending) return null
const file = data?.get('avatar')
const name = file instanceof File ? file.name : 'file'
return (
<p aria-live="polite">
Uploading {name} via {method.toUpperCase()}…
</p>
)
}
Discussion