Handling Form State with useActionState
useActionState (React 19, replacing useFormState) connects a Server Action to a Client Component, giving you access to the action's return value — error messages, success states, and field validation errors.
Server Action with State Return
// app/actions/auth.ts
'use server';
interface ActionState {
errors?: {
email?: string[];
password?: string[];
};
message?: string;
success?: boolean;
}
export async function loginAction(
prevState: ActionState,
formData: FormData
): Promise<ActionState> {
const email = formData.get('email') as string;
const password = formData.get('password') as string;
// Validate
const errors: ActionState['errors'] = {};
if (!email || !email.includes('@')) {
errors.email = ['Please enter a valid email address.'];
}
if (!password || password.length < 8) {
errors.password = ['Password must be at least 8 characters.'];
}
if (Object.keys(errors).length > 0) {
return { errors };
}
// Attempt login
const user = await verifyCredentials(email, password);
if (!user) {
return { message: 'Invalid email or password.' };
}
await createSession(user.id);
return { success: true };
}
Client Component using useActionState
// app/(auth)/login/page.tsx
'use client';
import { useActionState } from 'react';
import { loginAction } from '@/actions/auth';
const initialState = {};
export default function LoginPage() {
const [state, formAction] = useActionState(loginAction, initialState);
return (
<form action={formAction} className="max-w-sm mx-auto mt-16 space-y-4">
<h1 className="text-2xl font-bold">Sign In</h1>
{state.message && (
<div className="bg-red-50 text-red-700 p-3 rounded-md">
{state.message}
</div>
)}
<div>
<label className="block text-sm font-medium mb-1">Email</label>
<input
name="email"
type="email"
className="w-full border rounded-md px-3 py-2"
/>
{state.errors?.email?.map(error => (
<p key={error} className="text-red-600 text-sm mt-1">{error}</p>
))}
</div>
<div>
<label className="block text-sm font-medium mb-1">Password</label>
<input
name="password"
type="password"
className="w-full border rounded-md px-3 py-2"
/>
{state.errors?.password?.map(error => (
<p key={error} className="text-red-600 text-sm mt-1">{error}</p>
))}
</div>
<SubmitButton />
</form>
);
}
This pattern gives you server-side validation with per-field error messages without a single API route.