| |

React 56 ⚛️ Form Pending Status with useFormStatus

⏱️ Estimated read: 16 min | |

A form submission is asynchronous. The user clicks a button, the request goes out, and the UI must decide what to show while it waits. In React 19, that decision is made declaratively through the useFormStatus hook. It reads the pending state of the nearest parent <form> and exposes it to any child component, without prop drilling, without a global store, and without lifting state to the form’s parent. The hook exists because the pending state is inherently contextual: a submit button needs to know whether its form is submitting, and that information belongs to the form, not to the component that renders the button.

Key point: useFormStatus reads the status of a parent <form>. It must be called from a component rendered inside that form. A component that renders the form and calls the hook in the same component receives pending: false forever.


Why useFormStatus exists

The prop-drilling problem. Before React 19, pending state was managed with useState and passed as a prop from the form’s parent to the submit button. The button could be several levels deep, and every level had to forward the prop. The state lived in the wrong place: the form owns the submission, but the parent owned the flag.

The context problem. A submit button is a reusable component. It should not require a prop to know whether the form around it is submitting. The useFormStatus hook reads that information from React’s internal form context, so the button works anywhere inside a <form action={...}> without any wiring .

The form-action problem. React 19’s Actions system lets a function be passed directly to the <form> element’s action prop. When that function runs, React automatically tracks the pending state and provides it through useFormStatus. The hook is the read side of that mechanism .

The progressive-enhancement problem. Forms with an action prop work even before JavaScript loads, because the browser submits them natively. When JavaScript does load, useFormStatus provides the pending state that the native submission cannot express .

The separation-of-concerns problem. The component that owns the action (useActionState) and the component that shows the pending UI (useFormStatus) are different components. The form’s parent handles the result and error state; the submit button handles the pending label and disabled state. The two hooks split the responsibility cleanly .

The placement problem. The hook’s behavior is scoped: it only reads the parent form. This is deliberate. A component that renders a form should not read its own form’s status, because the status does not exist until the form is submitted, and the component would need to re-render to see it. Moving the reader into a child component makes the dependency explicit and correct .


a. The useFormStatus API

useFormStatus is imported from react-dom, not from react. It takes no parameters and returns a status object .

The status object

import { useFormStatus } from "react-dom";

function SubmitButton() {
  const status = useFormStatus();
  return <button disabled={status.pending}>Submit</button>;
}

The status object has four properties :

PropertyTypeDescription
pendingbooleantrue while the parent form is submitting
dataFormData | nullThe data being submitted
method'get' | 'post'The form’s HTTP method
actionfunction or nullThe function passed to the form’s action prop

The pending property is the one used most often. It drives the disabled state of the submit button and the label swap from “Submit” to “Submitting…” .

The placement rule

The hook must be called from a component that is a child of the <form> element whose status is being read .

function Form() {
  return (
    <form action={submitAction}>
      <input name="email" />
      <SubmitButton />  {/* useFormStatus works here */}
    </form>
  );
}

function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>Submit</button>;
}

The hook reads the status of the form that wraps the component. It does not read the status of a form rendered in the same component .

The same-component pitfall

Calling useFormStatus in the component that renders the form produces pending: false forever .

// ❌ WRONG: pending is always false
function BadForm() {
  const { pending } = useFormStatus();
  return (
    <form action={submitAction}>
      <button disabled={pending}>Submit</button>
    </form>
  );
}

The hook does not track a form rendered in the same component because the context is established by the <form> element, and the component that renders the form is outside that context .

The data property

The data property contains the FormData being submitted. It is null when there is no active submission .

function SubmitButton() {
  const { pending, data } = useFormStatus();
  const email = data?.get("email");
  return (
    <button disabled={pending}>
      {pending ? `Sending to ${email}…` : "Send"}
    </button>
  );
}

The data property is useful for showing a temporary message that confirms what the user is submitting .

The method and action properties

The method is 'get' or 'post', reflecting the form’s method attribute. The default is 'get' . The action is the function reference passed to the form’s action prop. It is null if the action is a URI string or if there is no parent form .


b. Common patterns

The hook serves a small set of recurring patterns. Each one places the hook in a child component and uses the status to drive the UI.

The pending button

The most common pattern is a submit button that disables itself and changes its label while the form submits.

import { useFormStatus } from "react-dom";

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

The aria-busy attribute announces the pending state to assistive technologies . The disabled attribute prevents double submission.

The form-level indicator

A component inside the form can show a broader indicator, such as a progress message or a spinner, while the form is submitting.

function FormStatus() {
  const { pending, data, method } = useFormStatus();
  if (!pending) return null;
  const email = data?.get("email");
  return <p>Submitting {email} via {method}…</p>;
}

This component renders nothing when the form is idle and a message when it is pending .

The input disabling pattern

Inputs inside the form can disable themselves while the form submits, preventing the user from editing the data mid-flight.

function FormInput({ name, ...props }) {
  const { pending } = useFormStatus();
  return <input name={name} disabled={pending} {...props} />;
}

The hook is called in the input component, which is a child of the form .

The progressive-enhancement pattern

A form with an action prop works without JavaScript. When JavaScript loads, useFormStatus provides the pending state. The two behaviors coexist: the native submission works, and the enhanced submission shows the pending UI .

The pairing with useActionState

useActionState manages the form’s return value and error state. useFormStatus manages the pending UI in the submit button. The two hooks are used together: the parent owns the action and its result, and the child owns the pending indicator .

function Form() {
  const [state, formAction] = useActionState(submitAction, { error: null });
  return (
    <form action={formAction}>
      <input name="email" />
      <SubmitButton />
      {state.error && <p>{state.error}</p>}
    </form>
  );
}

c. Caveats and limitations

useFormStatus is a focused hook with specific behavior. The caveats are what prevent the most common mistakes.

The parent-only scope

The hook reads only the parent form. It does not read the status of a form rendered in the same component, nor the status of child forms . This is by design: the hook is for the components inside a form, not for the component that renders it .

The no-parameters rule

The hook takes no arguments. There is no way to point it at a specific form. The form is determined by the component tree, not by a parameter .

The action-URI case

If the form’s action is a URI string rather than a function, status.action is null. The pending property still works if the form is submitted through React’s action system, but the action reference is unavailable .

The availability

useFormStatus was introduced in React 19 and is available in the stable release. In React 18.3 it was available only in the Canary and experimental channels .

The import source

The hook is imported from react-dom, not from react. This is because it reads DOM form context, which is provided by the react-dom package .


Complete Example Session

// ============================================
// PART 1: THE BASIC PENDING BUTTON
// ============================================

import { useFormStatus } from "react-dom";

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? "Submitting…" : "Submit"}
    </button>
  );
}

// ============================================
// PART 2: THE FORM THAT USES IT
// ============================================

async function submitAction(formData) {
  await fetch("/api/submit", { method: "POST", body: formData });
}

export default function Form() {
  return (
    <form action={submitAction}>
      <input name="email" type="email" />
      <SubmitButton />
    </form>
  );
}

// The hook reads the status of the form that wraps SubmitButton.
// No props are passed to SubmitButton.

// ============================================
// PART 3: THE SAME-COMPONENT PITFALL
// ============================================

// ❌ WRONG: pending is always false
function BadForm() {
  const { pending } = useFormStatus();
  return (
    <form action={submitAction}>
      <button disabled={pending}>Submit</button>
    </form>
  );
}

// ✅ CORRECT: extract the button
function GoodForm() {
  return (
    <form action={submitAction}>
      <SubmitButton />
    </form>
  );
}

// ============================================
// PART 4: READING THE SUBMITTED DATA
// ============================================

function StatusMessage() {
  const { pending, data, method } = useFormStatus();
  if (!pending) return null;
  const email = data?.get("email");
  return <p>Submitting {email} via {method}…</p>;
}

export default function FormWithStatus() {
  return (
    <form action={submitAction}>
      <input name="email" />
      <StatusMessage />
      <SubmitButton />
    </form>
  );
}

// ============================================
// PART 5: DISABLING INPUTS WHILE PENDING
// ============================================

function FormInput({ name, type = "text" }) {
  const { pending } = useFormStatus();
  return <input name={name} type={type} disabled={pending} />;
}

// ============================================
// PART 6: PAIRING WITH useActionState
// ============================================

import { useActionState } from "react";

async function createUser(prevState, formData) {
  const email = formData.get("email");
  if (!email.includes("@")) return { error: "Invalid email" };
  await saveUser(email);
  return { error: null, success: true };
}

function CreateUserForm() {
  const [state, formAction] = useActionState(createUser, { error: null });

  return (
    <form action={formAction}>
      <FormInput name="email" type="email" />
      <SubmitButton>Create user</SubmitButton>
      {state.error && <p role="alert">{state.error}</p>}
    </form>
  );
}

// useActionState owns the result.
// useFormStatus owns the pending UI.

// ============================================
// PART 7: THE ARIA-BUSY ATTRIBUTE
// ============================================

function AccessibleSubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending} aria-busy={pending}>
      {pending ? "Submitting…" : "Submit"}
    </button>
  );
}

// aria-busy tells screen readers the form is working.

// ============================================
// PART 8: THE NESTED FORM SCOPE
// ============================================

function InnerForm() {
  return (
    <form action={innerAction}>
      <SubmitButton />  {/* Reads the inner form's status */}
    </form>
  );
}

function OuterForm() {
  return (
    <form action={outerAction}>
      <input name="outer" />
      <InnerForm />  {/* The inner form has its own status */}
    </form>
  );
}

// Each SubmitButton reads the nearest parent form.
// The outer form's status is not visible inside the inner form.

// ============================================
// PART 9: THE IMPORT SOURCE
// ============================================

import { useFormStatus } from "react-dom";  // ✅ Correct
// import { useFormStatus } from "react";    // ❌ Wrong

// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================

function CompleteForm() {
  const [state, formAction] = useActionState(submitAction, { error: null });

  return (
    <form action={formAction}>
      <FormInput name="email" type="email" />
      <FormInput name="message" type="text" />
      <StatusMessage />
      <SubmitButton>Send message</SubmitButton>
      {state.error && <p role="alert">{state.error}</p>}
    </form>
  );
}

// The form owns the action and the result state.
// The children own the pending UI.
// No prop drilling.
// Progressive enhancement works.

The ten parts showed the basic pending button, the form that uses it, the same-component pitfall, reading submitted data, disabling inputs, pairing with useActionState, the aria-busy attribute, nested form scoping, the import source, and the complete pattern.


Quick Reference

The Status Object

PropertyTypeDescription
pendingbooleantrue while the parent form is submitting
dataFormData | nullThe data being submitted
method'get' | 'post'The form’s HTTP method
actionfunction or nullThe form’s action reference

Placement Rules

RuleReason
Call from a child of <form>The hook reads the parent form’s context
Not from the component that renders the formThe form’s context is not established there
Import from react-domThe hook reads DOM form context
Takes no parametersThe form is determined by the tree

Common Patterns

PatternHook Usage
Pending buttonpending for disabled and label
Form indicatorpending, data, method
Input disablingpending for disabled
Progressive enhancementWorks with action prop
Pairing with useActionStateParent owns result, child owns pending

The Pairing

HookOwnsImport From
useActionStateAction result, error statereact
useFormStatusPending UIreact-dom

Best Practices

✅ Do This:

// Call the hook in a child component of the form.
function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>Submit</button>;
}  // ✅

// Import from react-dom.
import { useFormStatus } from "react-dom";  // ✅

// Use aria-busy for accessibility.
<button disabled={pending} aria-busy={pending}>  // ✅

// Pair with useActionState for error handling.
const [state, formAction] = useActionState(action, {});  // ✅

// Read data for status messages.
const { pending, data } = useFormStatus();  // ✅

// Extract the button from the form component.
function Form() {
  return <form action={action}><SubmitButton /></form>;
}  // ✅

❌ Don’t Do This:

// Don't call the hook in the component that renders the form.
function Form() {
  const { pending } = useFormStatus();  // ❌ Always false
  return <form action={action}>...</form>;
}

// Don't import from react.
import { useFormStatus } from "react";  // ❌ Wrong package

// Don't pass pending as a prop.
<SubmitButton isPending={isPending} />  // ❌ useFormStatus replaces this

// Don't expect the hook to read child forms.
// It only reads the parent form.  // ❌

// Don't call the hook outside a form.
// pending is always false without a parent form.  // ❌

Common Pitfalls

PitfallWhy It HappensFix
pending is always falseHook called in the form’s own componentExtract the reader into a child
pending is always falseHook called outside a formMove the component inside <form>
Wrong importImported from reactImport from react-dom
Child form status readHook reads only the parentMove the hook inside the child form
action is nullForm uses a URI stringUse a function for action
Data is nullNo active submissionCheck pending first

Real-World Examples

1. Pending Submit Button

function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? "…" : "Submit"}</button>;
}

2. Form with Action

<form action={submitAction}>
  <input name="email" />
  <SubmitButton />
</form>

3. Status Message

function Status() {
  const { pending, data } = useFormStatus();
  if (!pending) return null;
  return <p>Sending to {data?.get("email")}…</p>;
}

4. Disabled Inputs

function Input({ name }) {
  const { pending } = useFormStatus();
  return <input name={name} disabled={pending} />;
}

5. Accessible Button

<button disabled={pending} aria-busy={pending}>  // ✅

6. Paired with useActionState

const [state, formAction] = useActionState(action, {});
<form action={formAction}><SubmitButton /></form>

7. Nested Forms

<form action={outer}>
  <form action={inner}><SubmitButton /></form>  {/* Reads inner */}
</form>

8. Import Source

import { useFormStatus } from "react-dom";

9. Same-Component Fix

// ❌ BadForm reads its own form.
// ✅ GoodForm extracts SubmitButton.

10. Progressive Enhancement

// The form works without JS.
// useFormStatus adds the pending UI when JS loads.

Visual

The Parent-Form Scope

┌────────────────────────────────────────────────────────────┐
│  useFormStatus READS THE PARENT FORM                       │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  <form action={submitAction}>                     │     │
│  │                                                   │     │
│  │    <input name="email" />                         │     │
│  │                                                   │     │
│  │    <SubmitButton />                               │     │
│  │      │                                            │     │
│  │      │  useFormStatus()                           │     │
│  │      │  reads the parent form's status            │     │
│  │      v                                            │     │
│  │    <button disabled={pending}>…</button>          │     │
│  │                                                   │     │
│  │  </form>                                          │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The hook reads the form that wraps the component.         │
│  It does not read forms in the same component.             │
└────────────────────────────────────────────────────────────┘

The Same-Component Pitfall

┌────────────────────────────────────────────────────────────┐
│  ❌ WRONG: HOOK IN THE FORM'S COMPONENT                    │
│                                                            │
│  function Form() {                                         │
│    const { pending } = useFormStatus();  // Always false   │
│    return (                                                │
│      <form action={action}>                                │
│        <button disabled={pending}>…</button>               │
│      </form>                                               │
│    );                                                      │
│  }                                                         │
│                                                            │
│  The component renders the form, so the form's context     │
│  does not exist when the hook runs.                        │
│                                                            │
│  ──────────────────────────────────────────────────────    │
│                                                            │
│  ✅ CORRECT: HOOK IN A CHILD COMPONENT                     │
│                                                            │
│  function Form() {                                         │
│    return (                                                │
│      <form action={action}>                                │
│        <SubmitButton />  {/* Inside the form */}           │
│      </form>                                               │
│    );                                                      │
│  }                                                         │
│                                                            │
│  function SubmitButton() {                                 │
│    const { pending } = useFormStatus();  // Works          │
│    return <button disabled={pending}>…</button>;           │
│  }                                                         │
└────────────────────────────────────────────────────────────┘

The Status Object

┌────────────────────────────────────────────────────────────┐
│  useFormStatus RETURN VALUE                                │
│                                                            │
│  const { pending, data, method, action } = useFormStatus() │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  pending  : boolean                               │     │
│  │             true while the form is submitting     │     │
│  │                                                   │     │
│  │  data     : FormData | null                       │     │
│  │             the data being submitted              │     │
│  │                                                   │     │
│  │  method   : 'get' | 'post'                        │     │
│  │             the form's HTTP method                │     │
│  │                                                   │     │
│  │  action   : function | null                       │     │
│  │             the form's action reference           │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  pending drives the disabled state and label.              │
│  data drives the status message.                           │
└────────────────────────────────────────────────────────────┘

Pairing with useActionState

┌────────────────────────────────────────────────────────────┐
│  useActionState + useFormStatus                            │
│                                                            │
│  PARENT (owns the action and result)                       │
│  ┌──────────────────────────────────────────────────┐     │
│  │  const [state, formAction] =                       │     │
│  │    useActionState(submitAction, { error: null });  │     │
│  │                                                   │     │
│  │  <form action={formAction}>                       │     │
│  │    <input name="email" />                         │     │
│  │    <SubmitButton />  ← child owns pending UI      │     │
│  │    {state.error && <p>{state.error}</p>}          │     │
│  │  </form>                                          │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  CHILD (owns the pending UI)                               │
│  ┌──────────────────────────────────────────────────┐     │
│  │  function SubmitButton() {                        │     │
│  │    const { pending } = useFormStatus();           │     │
│  │    return <button disabled={pending}>…</button>;  │     │
│  │  }                                                │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The parent owns the result.                               │
│  The child owns the pending state.                         │
│  No prop drilling.                                         │
└────────────────────────────────────────────────────────────┘

Summary

ItemValue
useFormStatusHook that reads the parent form’s submission status
ImportFrom react-dom
ParametersNone
Returns{ pending, data, method, action }
PlacementChild component inside <form>
Same-component pitfallpending is always false
pendingtrue while the form is submitting
dataThe FormData being submitted
PairingWorks with useActionState
Progressive enhancementWorks with the action prop

Key takeaways:

  • useFormStatus reads the parent form’s status. The hook must be called from a component rendered inside the <form> whose status is being read. It does not read forms rendered in the same component or child forms.
  • The same-component pitfall is the most common mistake. Calling useFormStatus in the component that renders the form produces pending: false forever. The fix is to extract the reader into a child component.
  • The hook takes no parameters. The form is determined by the component tree, not by an argument. The nearest parent form is the one that is tracked.
  • The import is from react-dom, not react. The hook reads DOM form context, which is provided by the react-dom package.
  • pending drives the submit button. The typical use is to disable the button and swap the label while the form is submitting, preventing double submission and giving the user feedback.
  • data drives the status message. The FormData being submitted can be read to show a temporary message that confirms what the user is sending.
  • The hook pairs with useActionState. The parent owns the action and its result state, and the child owns the pending UI. The two hooks split the responsibility cleanly without prop drilling.
  • Progressive enhancement works. A form with an action prop works without JavaScript. When JavaScript loads, useFormStatus provides the pending state that the native submission cannot express.

Remember: useFormStatus is the React 19 hook that gives any child component of a <form> access to the form’s submission status. It replaces the manual isSubmitting state and the prop drilling that the old pattern required. The hook reads the parent form, so the component that calls it must be rendered inside the form, not in the same component that renders the form. It returns pending, data, method, and action, and the pending property is what drives the disabled state and the label swap on the submit button. The hook pairs with useActionState, which owns the form’s result and error state, while useFormStatus owns the pending UI. The result is a form that shows immediate feedback during submission, prevents double submission, and works with progressive enhancement, all without a single prop passed from the form’s parent to the submit button.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!