Skip to main content

Managing Pending States: useFormStatus Guide

The useFormStatus hook gives you access to the pending status and form data of a parent form, without prop drilling or context. When a form is submitted via an action, useFormStatus instantly reflects that the form is pending—before the server responds. This is perfect for disabling buttons, showing spinners, or hiding secondary controls while a submission is in flight.

Unlike useActionState, which lives in the form component itself, useFormStatus can be called from deeply nested child components. This separation of concerns lets you build reusable button components, loading spinners, and status messages that automatically sync with the form's submission state.

The Basic Pattern: Button with useFormStatus

Here's the simplest use case—a submit button that disables itself during submission:

// Without useFormStatus
export default function LoginForm() {
const [state, dispatch, pending] = useActionState(login, {});

return (
<form action={dispatch}>
<input name="email" type="email" />
<input name="password" type="password" />
{/* Button must receive pending from parent */}
<button disabled={pending}>
{pending ? "Logging in..." : "Log In"}
</button>
</form>
);
}

With useFormStatus, you can lift the button into its own component and it automatically knows the pending state:

// components/SubmitButton.js
import { useFormStatus } from "react-dom";

export default function SubmitButton() {
const { pending } = useFormStatus();

return (
<button disabled={pending}>
{pending ? "Logging in..." : "Log In"}
</button>
);
}
// components/LoginForm.js
import { useActionState } from "react";
import { login } from "../actions";
import SubmitButton from "./SubmitButton";

export default function LoginForm() {
const [state, dispatch] = useActionState(login, {});

return (
<form action={dispatch}>
<input name="email" type="email" />
<input name="password" type="password" />
<SubmitButton />
</form>
);
}

The button automatically knows it's inside a form and that the form is pending. No props, no lifting state. useFormStatus returns an object with:

  • pending boolean: True while the form is submitting.
  • data FormData: The form's submitted data (useful for showing values).
  • method string: The HTTP method (GET or POST).
  • action function | null: The action function (usually a Server Action).

Accessing Form Data with useFormStatus

useFormStatus gives you the submitted form data, which is useful for displaying it while the submission is in progress:

import { useFormStatus } from "react-dom";

export default function LoadingIndicator() {
const { pending, data } = useFormStatus();

if (!pending) return null;

const formText = data?.get("message");

return (
<div style={{ opacity: 0.6, fontStyle: "italic" }}>
Posting: "{formText}"...
</div>
);
}

When the user submits a form with <input name="message" />, the LoadingIndicator shows the message being posted in real-time. The data object is a FormData instance, so use .get(name) to access field values.

Disabling All Form Fields During Submission

useFormStatus makes it easy to disable an entire form during submission. Use it to disable fieldsets or wrap inputs:

import { useFormStatus } from "react-dom";

export default function FormFieldset({ children, legend }) {
const { pending } = useFormStatus();

return (
<fieldset disabled={pending}>
<legend>{legend}</legend>
{children}
</fieldset>
);
}

Then use it in your form:

export default function CheckoutForm() {
const [state, dispatch] = useActionState(processPayment, {});

return (
<form action={dispatch}>
<FormFieldset legend="Shipping">
<input name="address" />
<input name="city" />
</FormFieldset>

<FormFieldset legend="Payment">
<input name="cardNumber" />
<input name="expiry" />
</FormFieldset>

<SubmitButton />
</form>
);
}

Now all inputs are disabled while payment is processing, preventing accidental double-submissions.

Building a Reusable Loading Spinner Component

Create a spinner that appears only when a form is pending:

// components/FormSpinner.js
import { useFormStatus } from "react-dom";

export default function FormSpinner({ message = "Loading..." }) {
const { pending } = useFormStatus();

if (!pending) return null;

return (
<div style={{
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
background: "rgba(0, 0, 0, 0.7)",
color: "white",
padding: "20px",
borderRadius: "8px",
zIndex: 1000
}}>
<div style={{ marginBottom: "10px" }}></div>
<p>{message}</p>
</div>
);
}

Use it inside the form:

export default function MyForm() {
const [state, dispatch] = useActionState(action, {});

return (
<form action={dispatch}>
<input name="title" />
<button>Submit</button>
<FormSpinner message="Saving your changes..." />
</form>
);
}

The spinner auto-mounts and auto-unmounts based on the form's pending state.

Conditional Rendering Based on Submission State

Use useFormStatus to show different UI based on whether the form is pending or has errors:

import { useFormStatus } from "react-dom";

export default function FormStatus({ state }) {
const { pending } = useFormStatus();

if (pending) {
return <p style={{ color: "blue" }}>Submitting...</p>;
}

if (state?.error) {
return <p style={{ color: "red" }}>{state.error}</p>;
}

if (state?.success) {
return <p style={{ color: "green" }}>Success!</p>;
}

return null;
}

This component shows different messages depending on the state. Pass state from useActionState and pending from useFormStatus.

Advanced: Multiple Actions in One Form

If a form has multiple submit buttons (each with a different action), useFormStatus knows which action was triggered:

async function saveDraft(prevState, formData) {
// Save to drafts...
return { saved: true };
}

async function publish(prevState, formData) {
// Publish for real...
return { published: true };
}

export default function Editor() {
const [state, dispatch] = useActionState(publish, {});

return (
<form action={dispatch}>
<textarea name="content" />

{/* First button uses dispatch */}
<button formAction={saveDraft}>Save Draft</button>

{/* Second button uses different action */}
<button type="submit">Publish</button>
</form>
);
}

Each button can have its own formAction, and useFormStatus reports which one is pending. (Note: formAction on a button overrides the form's action.)

Performance: useFormStatus Doesn't Cause Re-renders Outside the Form

useFormStatus only triggers updates in components inside the form. Components outside the form's tree won't re-render, even if the form is pending. This keeps performance high:

// This re-renders when form is pending (inside form)
function SubmitButton() {
const { pending } = useFormStatus(); // Will re-render
return <button disabled={pending}>Submit</button>;
}

// This does NOT re-render when form is pending (outside form)
function PageHeader() {
return <h1>My Page</h1>; // Won't re-render
}

export default function App() {
const [state, dispatch] = useActionState(action, {});

return (
<>
<PageHeader />
<form action={dispatch}>
<input name="text" />
<SubmitButton />
</form>
</>
);
}

Only the button inside the form re-renders when pending changes.

Key Takeaways

  • useFormStatus gives you pending and data from a parent form without prop drilling.
  • Use pending to disable buttons, disable fieldsets, or show loading indicators.
  • Use data to display submitted values in real-time while the submission is in flight.
  • useFormStatus works only inside a form element; calling it outside returns dummy values.
  • It's perfect for building reusable button, spinner, and status message components.

Frequently Asked Questions

What if I call useFormStatus outside a form?

It returns { pending: false, data: null, method: "GET", action: null }. Always check pending is true before relying on data.

Can I use useFormStatus with multiple forms on the same page?

Each component only sees the nearest ancestor form. If you have two forms and a component inside both, it sees the innermost form. Use multiple instances of the component if needed.

Does useFormStatus work with non-action forms?

No. useFormStatus only works when the form uses action={dispatch} where dispatch is from useActionState. Traditional onSubmit handlers don't trigger useFormStatus.

How do I show success after the form submits?

Use useFormStatus for pending state and access state from useActionState for success/error. Combine them in a status component.

Can I prevent form submission with useFormStatus?

No. useFormStatus is read-only—it reflects the form's state but doesn't control it. Prevent submission by disabling the button or validating in the action.

Further Reading