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:
pendingboolean: True while the form is submitting.dataFormData: The form's submitted data (useful for showing values).methodstring: The HTTP method (GET or POST).actionfunction | 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
useFormStatusgives youpendinganddatafrom a parent form without prop drilling.- Use
pendingto disable buttons, disable fieldsets, or show loading indicators. - Use
datato display submitted values in real-time while the submission is in flight. useFormStatusworks 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.