--- name: forms description: "Use when building forms - covers React Hook Form, Zod validation, and form patterns" --- Source Cursor rule: `.cursor/rules/forms.mdc`. Original Cursor alwaysApply: `false`. # Forms: React Hook Form + Zod **All forms MUST use React Hook Form with Zod validation.** ## Basic Pattern ```tsx import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { Button, Input } from '@trycompai/design-system'; // 1. Define schema const formSchema = z.object({ email: z.string().email('Invalid email'), password: z.string().min(8, 'Min 8 characters'), }); // 2. Infer type type FormData = z.infer; // 3. Use in component function MyForm() { const { register, handleSubmit, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(formSchema), }); return (
{errors.email &&

{errors.email.message}

}
); } ``` ## Zod Schema Patterns ```tsx const profileSchema = z.object({ // Strings name: z.string().min(1, 'Required'), email: z.string().email(), website: z.string().url().optional(), // Numbers (coerce for inputs) age: z.coerce.number().int().min(0), price: z.coerce.number().positive(), // Arrays tags: z.array(z.string()).min(1), // Enums status: z.enum(['active', 'inactive']), }); // Cross-field validation const passwordSchema = z.object({ password: z.string().min(8), confirmPassword: z.string(), }).refine(d => d.password === d.confirmPassword, { message: "Passwords don't match", path: ['confirmPassword'], }); ``` ## Controller for Complex Components ```tsx import { Controller } from 'react-hook-form'; import { Select, SelectContent, SelectItem, SelectTrigger } from '@trycompai/design-system'; ( )} /> ``` ## Form State ```tsx const { register, handleSubmit, control, watch, // Watch field values setValue, // Set field programmatically reset, // Reset form setError, // Set error manually formState: { errors, // Field errors isSubmitting, // Submitting isValid, // All valid isDirty, // Modified }, } = useForm({ resolver: zodResolver(schema), mode: 'onChange', // Validate on change }); ``` ## Error Handling ```tsx const onSubmit = async (data: FormData) => { try { await submitToApi(data); } catch (error) { // Field-specific error setError('email', { message: 'Email taken' }); // Or root error setError('root', { message: 'Something went wrong' }); } }; // Display root error {errors.root &&

{errors.root.message}

} ``` ## Dynamic Fields ```tsx import { useFieldArray } from 'react-hook-form'; const { fields, append, remove } = useFieldArray({ control, name: 'items', }); {fields.map((field, index) => (
))} ``` ## Anti-Patterns ```tsx // ❌ useState for form fields const [email, setEmail] = useState(''); // ❌ Manual validation if (email.length < 5) setError('Too short'); // ❌ Missing button type (defaults to submit) // ✅ Correct const { register } = useForm(); const schema = z.object({ email: z.string().min(5) }); ```