Mastering useFormStatus Hook in React: Build a Reusable Submit Button
The useFormStatus hook from React DOM is a game-changer for handling form submissions. It lets child components inside a form detect the form’s current status—like whether it’s pending submission—without prop drilling.
Introduced in React 19 (and available experimentally in 18.3+), it reads the closest form’s state. Perfect for loading indicators on buttons or spinners during async actions.
How useFormStatus Works
useFormStatus returns an object with these key properties:
pending: Boolean—true while the form’sactionfunction runsdata: FormData object with current form valuesmethod: The form’s submission method (GET/POST/etc.)state: Custom state from your action functionaction: The action function itself
Important: It only works inside a
<form>with anactionprop (server action or function).
Real-World Usecase: Reusable SubmitButton
"use client";
import { useFormStatus } from "react-dom";
interface SubmitButtonProps {
label: string;
loadingLabel?: string;
className?: string;
}
export function SubmitButton({
label,
loadingLabel = "Processing...",
className = "as-btn w-full text-sm font-medium",
}: SubmitButtonProps) {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
className={`${className} ${pending ? "opacity-70 cursor-not-allowed" : ""}`}
>
{pending ? loadingLabel : label}
</button>
);
}
Key Features:
- Auto-detects parent form status—no props needed
pendingdisables button + swaps text during submission- Tailwind classes for visual feedback
- Fully reusable across any form
Complete Working Example
// app/signup/page.tsx
import { SubmitButton } from "@/components/SubmitButton";
export default function SignupPage() {
async function createUser(formData: FormData) {
await new Promise((resolve) => setTimeout(resolve, 2000));
console.log("User created:", formData.get("email"));
}
return (
<form action={createUser} className="space-y-4 p-6 max-w-md mx-auto">
<input
name="email"
type="email"
placeholder="Email"
required
className="w-full p-2 border"
/>
<input
name="password"
type="password"
placeholder="Password"
required
className="w-full p-2 border"
/>
<SubmitButton
label="Create Account"
loadingLabel="Creating..."
/>
</form>
);
}
Pro Tips & Gotchas
- Placement: Must be direct/indirect child of
<form> - Multiple Forms: Detects nearest ancestor form
- Server Actions: Best with Next.js app router
actionfunctions - React 18: Needs
experimental_useFormStatusflag
// React 18 experimental setup
const formConfig = {
experimental_useFormStatus: true
};
Why You’ll Love It
No brittle loading props. No Context providers. Components stay focused. Scales perfectly for complex forms, modals, and wizards.
Try it today—your UX (and future self) will thank you!