Form Handling with useActionState, useFormStatus & Zod Validation0%
Form Handling with useActionState, useFormStatus & Zod Validation
Beginner12 min read•Updated: Jul 11, 2026
Form Handling with useActionState, useFormStatus & Zod Validation
Building professional forms in Next.js 15 requires combining React 19's useActionState (for managing action state and error responses), useFormStatus (for loading and pending states), and Zod (for type-safe schema validation).
1. The React 19 / Next.js 15 Form Stack
useActionState: Hook that wraps a Server Action to track return values, validation errors, and pending state across submissions.useFormStatus: Hook that provides the pending status of a parent<form>, allowing buttons to display spinners during submission.- Zod Validation: Validates form fields on the server before database mutation.
2. Server Action with Zod Validation
TypeScript
// app/actions/auth-actions.ts
'use server';
import { z } from 'zod';
const signupSchema = z.object({
email: z.string().email('Invalid email address format'),
password: z.string().min(8, 'Password must be at least 8 characters long')
});
export type ActionState = {
errors?: {
email?: string[];
password?: string[];
};
message?: string;
};
export async function signupUser(prevState: ActionState, formData: FormData): Promise<ActionState> {
const result = signupSchema.safeParse({
email: formData.get('email'),
password: formData.get('password')
});
if (!result.success) {
return {
errors: result.error.flatten().fieldErrors
};
}
// Database registration logic...
return { message: 'Account successfully registered!' };
}
3. The Client Form Component with useActionState
TSX
// components/SignupForm.tsx
'use client';
import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
import { signupUser, type ActionState } from '@/app/actions/auth-actions';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button
type="submit"
disabled={pending}
className="w-full bg-blue-600 text-white py-2 rounded disabled:bg-slate-400"
>
{pending ? 'Submitting...' : 'Sign Up'}
</button>
);
}
export default function SignupForm() {
const initialState: ActionState = {};
const [state, formAction] = useActionState(signupUser, initialState);
return (
<form action={formAction} className="max-w-md mx-auto space-y-4">
{state.message && <p className="text-green-600 font-medium">{state.message}</p>}
<div>
<label className="block text-sm font-medium">Email</label>
<input name="email" type="email" className="w-full border p-2 rounded" />
{state.errors?.email && (
<p className="text-red-500 text-sm mt-1">{state.errors.email[0]}</p>
)}
</div>
<div>
<label className="block text-sm font-medium">Password</label>
<input name="password" type="password" className="w-full border p-2 rounded" />
{state.errors?.password && (
<p className="text-red-500 text-sm mt-1">{state.errors.password[0]}</p>
)}
</div>
<SubmitButton />
</form>
);
}
Interactive Quiz
Multiple Choice Questions
Question 1
Which React 19 hook replaces the experimental
useFormState in Next.js 15 for managing server action return states and errors?Question 2
Where must the
useFormStatus hook be called to accurately detect if a form submission is pending?Question 3
What method on a Zod schema validates data without throwing exceptions, returning an object with
{ success: true, data } or { success: false, error }?Question 4
What is the first argument passed to a Server Action when wired through
useActionState?Question 5
Why is server-side validation with Zod necessary even if the client form includes HTML5
required and type="email" attributes?Next Lesson
On-Demand Cache Revalidation with revalidatePath and revalidateTag
Continue learning with hands-on practice, examples, and exercises in the upcoming topic.
Related Lessons
Practice Quiz
Test your understanding of this lesson with 5 questions. Each question has one correct answer.