useFormStatus

useFormStatus() is a Hook that reports the status of the nearest parent <form>'s most recent submission. Unlike the other Hooks on this page, it's imported from 'react-dom', not 'react'. It takes no arguments, and returns an object with four properties: pending (true while that form's submission is in flight), data (a FormData instance with the values being submitted, or null), method ('get' or 'post'), and action (a reference to the function passed to the form's action prop, or null).

The one rule that matters most: useFormStatus() only reports on a <form> that is an ancestor of the component calling it. Calling it in the very same component that renders the <form> — instead of in a separate child component nested inside that <form> — always returns the default idle status, since from that component's position in the tree, there is no parent form yet to report on.

Because of that rule, useFormStatus() is typically pulled out into its own small component, such as a reusable submit button, and rendered as a child wherever a <form> needs one — commonly a form whose action was created with useActionState(), covered earlier.

Click Subscribe. The button, a child of
, correctly shows 'Submitting...' for one second. The status line above the form is read in App itself — the same component that renders the
, not a child of it — so it stays stuck on 'pending = false' the whole time.

RESETRUNFULL
<!DOCTYPE html><html><head>
<script type="importmap">
{"imports": {"react": "https://esm.sh/react@19", "react-dom": "https://esm.sh/react-dom@19", "react-dom/client": "https://esm.sh/react-dom@19/client"}}
</script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head><body>
<div id="root"></div>
<script type="text/babel" data-type="module" data-presets="react">
import { useFormStatus } from 'react-dom';
import { createRoot } from 'react-dom/client';

async function submitAction(formData) {
  await new Promise((resolve) => setTimeout(resolve, 1000)); // simulate a slow submission
  console.log('Submitted:', formData.get('email'));
}

function SubmitButton() {
  // Correct: SubmitButton is a CHILD of <form>, so it sees the form's real status.
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? 'Submitting...' : 'Subscribe'}</button>;
}

function App() {
  // Wrong (for comparison): this is the SAME component that renders <form> below,
  // not a child of it, so this 'pending' is always false.
  const { pending } = useFormStatus();
  return (
    <div>
      <p>Status read in App itself: pending = {String(pending)}</p>
      <form action={submitAction}>
        <input type="email" name="email" required placeholder="you@example.com" />
        <SubmitButton />
      </form>
    </div>
  );
}
createRoot(document.getElementById('root')).render(<App />);
</script>
</body></html>