Careeroza
useActionState and useFormStatus
Returning Errors to the Form
useActionState (React 19) keeps the last value returned by the action, so you can show errors and messages. The action receives the previous state as its first argument.
// app/contact/actions.ts
'use server';
export type ContactState = { ok: boolean; message: string };
export async function sendMessage(prev: ContactState, formData: FormData): Promise<ContactState> {
const email = String(formData.get('email') ?? '');
if (!email.includes('@')) return { ok: false, message: 'Enter a valid email' };
// ...save to DB / send email
return { ok: true, message: 'Thanks! We will reply within 24 hours.' };
}// app/contact/ContactForm.tsx
'use client';
import { useActionState } from 'react';
import { sendMessage, type ContactState } from './actions';
const initial: ContactState = { ok: false, message: '' };
export default function ContactForm() {
const [state, formAction, pending] = useActionState(sendMessage, initial);
return (
<form action={formAction}>
<input name="email" type="email" placeholder="[email protected]" />
<textarea name="message" />
<button disabled={pending}>{pending ? 'Sending...' : 'Send'}</button>
{state.message && <p className={state.ok ? 'text-green-600' : 'text-red-600'}>{state.message}</p>}
</form>
);
}useFormStatus
useFormStatus from react-dom tells a component inside a <form> whether that form is submitting. Handy for a reusable submit button:
'use client';
import { useFormStatus } from 'react-dom';
export function SubmitButton({ label }: { label: string }) {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending} aria-busy={pending}>
{pending ? 'Please wait...' : label}
</button>
);
}Note: useFormStatus only works in a component rendered inside the form, not in the component that renders the form.