React 19: useFormStatus for Form State Access
Learn to use useFormStatus to access form submission status from child components without prop drilling.
Topics Covered:
Prerequisites:
- React 19: The use Hook - Reading Promises and Context
Overview
useFormStatus is React 19's hook for accessing form submission status from child components. It eliminates the need to pass pending state through props, enabling better component composition. This tutorial covers when and how to use useFormStatus effectively, its limitations, and best practices for form state management.
Lesson 1: Understanding useFormStatus
useFormStatus provides access to the nearest form's submission status. Key Features: • Accesses form status automatically • No prop drilling needed • Works with Server Actions • Must be used inside a form Limitations: • Only works inside <form> elements • Requires Server Action or form action • Cannot be used outside form context When to Use: • Submit buttons in forms • Form validation messages • Loading indicators • Avoiding prop drilling When NOT to Use: • Outside form elements • Simple controlled forms • When prop passing is clear
// Basic usage
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : 'Submit'}
</button>
);
}
function Form() {
return (
<form action={serverAction}>
<input name="email" />
<SubmitButton /> {/* Automatically knows form status */}
</form>
);
}
// Without useFormStatus (prop drilling)
function FormWithoutHook() {
const [pending, setPending] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setPending(true);
await serverAction(new FormData(e.currentTarget));
setPending(false);
}
return (
<form onSubmit={handleSubmit}>
<input name="email" />
<SubmitButton pending={pending} /> {/* Must pass prop */}
</form>
);
}
// With useFormStatus (no props needed)
function FormWithHook() {
return (
<form action={serverAction}>
<input name="email" />
<SubmitButton /> {/* No props! */}
</form>
);
}useFormStatus eliminates prop drilling by automatically detecting form submission status. The component must be inside a form element that uses a Server Action.
Lesson 2: useFormStatus API
useFormStatus returns an object with form submission information. Returned Properties: • pending: Boolean - true when form is submitting • data: FormData - current form data being submitted • method: 'get' | 'post' - HTTP method • action: string | Function - form action Hook Location: • Must be inside a <form> element • Can be deeply nested • Accesses nearest parent form • Throws error if used outside form
import { useFormStatus } from 'react-dom';
function FormStatusDisplay() {
const { pending, data, method, action } = useFormStatus();
return (
<div>
{pending && (
<div>
<p>Submitting form...</p>
{data && (
<p>Email: {data.get('email')}</p>
)}
<p>Method: {method}</p>
</div>
)}
</div>
);
}
function ComplexForm() {
return (
<form action={serverAction} method="post">
<div>
<label>Email</label>
<input name="email" type="email" />
</div>
<div>
<label>Message</label>
<textarea name="message" />
</div>
{/* Deeply nested, still works */}
<div>
<div>
<FormStatusDisplay /> {/* Accesses parent form */}
<SubmitButton />
</div>
</div>
</form>
);
}
// Accessing form data
function FormDataDisplay() {
const { pending, data } = useFormStatus();
if (!pending || !data) {
return null;
}
return (
<div className="form-data-preview">
<p>Submitting:</p>
<ul>
{Array.from(data.entries()).map(([key, value]) => (
<li key={key}>
{key}: {String(value)}
</li>
))}
</ul>
</div>
);
}
// Error handling
function SafeFormStatus() {
try {
const { pending } = useFormStatus();
return pending ? <Spinner /> : null;
} catch (error) {
// Not inside a form
return null;
}
}useFormStatus provides pending status, form data, method, and action. Use these properties to create dynamic form UI that responds to submission state.
Lesson 3: Practical Use Cases
Explore real-world use cases for useFormStatus. Common Patterns: • Submit buttons with loading states • Form validation messages • Progress indicators • Disabled inputs during submission • Success/error messages Best Practices: • Use in submit buttons • Disable inputs during submission • Show loading indicators • Provide user feedback • Handle errors gracefully
// 1. Submit button with loading
function SubmitButton({ children }: { children: React.ReactNode }) {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? (
<>
<Spinner />
Submitting...
</>
) : (
children
)}
</button>
);
}
// 2. Disable inputs during submission
function FormInput({ name, ...props }: InputProps) {
const { pending } = useFormStatus();
return (
<input
name={name}
disabled={pending}
aria-disabled={pending}
{...props}
/>
);
}
// 3. Form-wide loading overlay
function FormOverlay() {
const { pending } = useFormStatus();
if (!pending) return null;
return (
<div className="form-overlay">
<Spinner />
<p>Submitting form...</p>
</div>
);
}
// 4. Character counter with submission state
function TextAreaWithCounter({
name,
maxLength
}: {
name: string;
maxLength: number;
}) {
const { pending, data } = useFormStatus();
const currentValue = data?.get(name)?.toString() || '';
const remaining = maxLength - currentValue.length;
return (
<div>
<textarea
name={name}
maxLength={maxLength}
disabled={pending}
/>
<span className={remaining < 10 ? 'warning' : ''}>
{remaining} characters remaining
</span>
{pending && <span className="submitting">Saving...</span>}
</div>
);
}
// 5. Complete form example
function ContactForm() {
return (
<form action={submitContactForm}>
<FormOverlay />
<div>
<label>Name</label>
<FormInput name="name" required />
</div>
<div>
<label>Email</label>
<FormInput name="email" type="email" required />
</div>
<div>
<label>Message</label>
<TextAreaWithCounter name="message" maxLength={500} />
</div>
<SubmitButton>Send Message</SubmitButton>
<FormStatusDisplay />
</form>
);
}
// 6. Multi-step form with status
function MultiStepForm() {
const [step, setStep] = useState(1);
const { pending } = useFormStatus();
return (
<form action={handleSubmit}>
{step === 1 && <Step1 />}
{step === 2 && <Step2 />}
{step === 3 && <Step3 />}
<div className="form-actions">
{step > 1 && (
<button
type="button"
onClick={() => setStep(s => s - 1)}
disabled={pending}
>
Back
</button>
)}
<SubmitButton>
{step < 3 ? 'Next' : 'Submit'}
</SubmitButton>
</div>
{pending && <ProgressBar />}
</form>
);
}Common use cases include submit buttons, input disabling, loading indicators, and form-wide overlays. useFormStatus simplifies all of these patterns.
Lesson 4: Combining with useActionState
useFormStatus works seamlessly with useActionState for complete form state management. Combination Pattern: • useActionState: Form state and action • useFormStatus: Access pending from children • Result: Clean, composable forms Benefits: • No prop drilling • Better component composition • Automatic status detection • Type-safe form handling
// Complete form with useActionState + useFormStatus
import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
function ContactForm() {
const [state, formAction, pending] = useActionState(
submitContact,
{ message: '', error: '' }
);
return (
<form action={formAction}>
<FormInput name="name" />
<FormInput name="email" type="email" />
{state.error && (
<ErrorMessage>{state.error}</ErrorMessage>
)}
{state.message && (
<SuccessMessage>{state.message}</SuccessMessage>
)}
{/* Child component automatically knows form status */}
<SubmitButton>Submit</SubmitButton>
{/* Can also use pending from useActionState */}
{pending && <FormOverlay />}
</form>
);
}
// SubmitButton uses useFormStatus (no props needed)
function SubmitButton({ children }: { children: React.ReactNode }) {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Submitting...' : children}
</button>
);
}
// FormInput also uses useFormStatus
function FormInput(props: InputProps) {
const { pending } = useFormStatus();
return (
<input
{...props}
disabled={pending || props.disabled}
/>
);
}
// Benefits:
// - No need to pass pending to every child
// - Components are reusable
// - Cleaner component tree
// - Automatic status synchronizationCombining useActionState with useFormStatus provides complete form state management without prop drilling. Child components automatically access form status.
Lesson 5: Limitations and Workarounds
Understand useFormStatus limitations and how to work around them. Limitations: • Must be inside <form> element • Requires Server Action • Cannot access form state outside form • No access to validation state Workarounds: • Use useActionState for form state • Combine with other hooks • Use Error Boundaries for errors • Create wrapper components
// Limitation: Must be inside form
// ❌ This won't work
function App() {
return (
<div>
<Form />
<FormStatus /> {/* Error: Not inside form */}
</div>
);
}
// ✅ Workaround: Move inside form
function App() {
return (
<Form>
<FormStatus /> {/* Works */}
</Form>
);
}
// Limitation: Only works with Server Actions
// ❌ Won't work with regular form handlers
function Form() {
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
// Regular form handler
}
return (
<form onSubmit={handleSubmit}>
<SubmitButton /> {/* useFormStatus won't work */}
</form>
);
}
// ✅ Use Server Actions instead
'use server';
async function submitForm(formData: FormData) {
// Server action
}
function Form() {
return (
<form action={submitForm}>
<SubmitButton /> {/* Works with useFormStatus */}
</form>
);
}
// Accessing form state outside form
function FormWrapper() {
const [state, formAction, pending] = useActionState(submitForm, {});
return (
<div>
<Form formAction={formAction} />
{/* Can access state here */}
{state.error && <ErrorMessage>{state.error}</ErrorMessage>}
</div>
);
}
// Combining multiple hooks
function AdvancedForm() {
const [state, formAction] = useActionState(submitForm, {});
const [optimisticState, addOptimistic] = useOptimistic(state, updateFn);
return (
<form action={formAction}>
{/* useFormStatus works here */}
<SubmitButton />
{/* Can also use state from useActionState */}
{state.error && <ErrorDisplay error={state.error} />}
</form>
);
}Understand useFormStatus limitations. It requires a form element and Server Action. For complex scenarios, combine with useActionState or other hooks.
Conclusion
useFormStatus eliminates prop drilling for form submission status. It enables better component composition and cleaner code. Always use it inside form elements with Server Actions. Combine with useActionState for complete form state management.