In our previous post, we looked at how the new React Actions API lets you handle forms with simple, server-first functions — no onSubmit
, no fetch
. But what about user feedback?
Let’s look at how to handle:
- ✅ Loading indicators
- 🚫 Validation errors
- 🎉 Success messages
⏳ Handling Loading States with useFormStatus
React 19 introduces useFormStatus
, a new hook that lets you track form submission status directly inside your components — no need to manage your own loading state.
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Sending...' : 'Send'}
</button>
);
}
Use this button inside a form that uses a server action:
<form action={sendContact}>
<input name="email" />
<textarea name="message" />
<SubmitButton />
</form>
⚠️ Displaying Validation Errors
You can throw errors from inside your action — and then catch them client-side with React’s new error handling patterns.
Let’s update our server action:
'use server';
export async function sendContact(formData: FormData) {
const email = formData.get('email');
if (!email || !email.toString().includes('@')) {
throw new Error('Invalid email address');
}
// Continue processing...
}
And now show the error in your component using useFormState
:
'use client';
import { useFormState } from 'react-dom';
import { sendContact } from '../actions';
function ContactForm() {
const [state, formAction] = useFormState(sendContact, null);
return (
<form action={formAction}>
<input name="email" />
<textarea name="message" />
{state && <p className="error">{state}</p>}
<SubmitButton />
</form>
);
}
You just pass the sendContact
function to useFormState
— it handles calling the server and passing back errors or results.
Modify your action to return a string instead of throwing if you prefer:
export async function sendContact(_: FormData): Promise<string | null> {
return 'Oops, something went wrong!';
}
🎉 Showing Success Messages
You can also return a success message or redirect after the form completes. Example:
export async function sendContact(formData: FormData) {
// ... handle form
return 'Thank you! Your message was sent.';
}
And then use it in your component like this:
const [message, formAction] = useFormState(sendContact, null);
return (
<form action={formAction}>
{/* form fields */}
<SubmitButton />
{message && <p className="success">{message}</p>}
</form>
);
✅ Final Thoughts
With useFormState
and useFormStatus
, you can now build robust form UIs with built-in loading and error handling — all while keeping your business logic on the server.
No more local loading
state, no more useEffect
hacks, no more mixing server/client responsibilities. Just clean, declarative form logic.
Let me know if you’d like a deep dive on:
- 🧪 Validating multiple fields with zod/yup
- 🌐 Redirecting after form submission
- 🔄 Resetting forms after success
Happy coding with React Actions!