--- name: form-state-patterns description: React Hook Form v7 with Zod validation, React 19 useActionState, Server Actions, field arrays, and async validation. Use when building complex forms, validation flows, or server action forms. tags: [react-hook-form, zod, forms, validation, server-actions, field-arrays, useActionState] context: fork agent: frontend-ui-developer version: 1.0.0 allowed-tools: [Read, Write, Grep, Glob] author: OrchestKit user-invocable: false --- # Form State Patterns Production form patterns with React Hook Form v7 + Zod - type-safe, performant, accessible. ## Overview - Complex forms with validation - Multi-step wizards - Dynamic field arrays - Server-side validation - Async field validation - Forms with file uploads ## Core Patterns ### 1. Basic Form with Zod Schema ```typescript import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; const userSchema = z.object({ email: z.string().email('Invalid email'), password: z.string().min(8, 'Min 8 characters'), confirmPassword: z.string(), }).refine((data) => data.password === data.confirmPassword, { message: "Passwords don't match", path: ['confirmPassword'], }); type UserForm = z.infer; function SignupForm() { const { register, handleSubmit, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(userSchema), defaultValues: { email: '', password: '', confirmPassword: '' }, }); const onSubmit = async (data: UserForm) => { await api.signup(data); }; return (
{errors.email && {errors.email.message}} {errors.password && {errors.password.message}} {errors.confirmPassword && {errors.confirmPassword.message}}
); } ``` ### 2. Field Arrays (Dynamic Fields) ```typescript import { useFieldArray, useForm } from 'react-hook-form'; const orderSchema = z.object({ items: z.array(z.object({ productId: z.string().min(1), quantity: z.number().min(1).max(100), })).min(1, 'At least one item required'), }); function OrderForm() { const { control, register, handleSubmit } = useForm({ resolver: zodResolver(orderSchema), defaultValues: { items: [{ productId: '', quantity: 1 }] }, }); const { fields, append, remove } = useFieldArray({ control, name: 'items', }); return (
{fields.map((field, index) => (
))}
); } ``` ### 3. Async Field Validation ```typescript const usernameSchema = z.object({ username: z.string() .min(3) .refine(async (value) => { const available = await checkUsernameAvailability(value); return available; }, 'Username already taken'), }); // Or with mode: 'onBlur' for better UX const { register } = useForm({ resolver: zodResolver(usernameSchema), mode: 'onBlur', // Validate on blur, not on every keystroke }); ``` ### 4. Server Actions (React 19 / Next.js) ```typescript // actions.ts 'use server'; import { z } from 'zod'; const contactSchema = z.object({ name: z.string().min(2), email: z.string().email(), message: z.string().min(10), }); export async function submitContact(formData: FormData) { const result = contactSchema.safeParse({ name: formData.get('name'), email: formData.get('email'), message: formData.get('message'), }); if (!result.success) { return { errors: result.error.flatten().fieldErrors }; } await saveContact(result.data); return { success: true }; } // Component 'use client'; import { useActionState } from 'react'; import { submitContact } from './actions'; function ContactForm() { const [state, formAction, isPending] = useActionState(submitContact, null); return (
{state?.errors?.name && {state.errors.name[0]}} {state?.errors?.email && {state.errors.email[0]}}