| 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
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Button, Input } from '@trycompai/design-system';
const formSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Min 8 characters'),
});
type FormData = z.infer<typeof formSchema>;
function MyForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>({
resolver: zodResolver(formSchema),
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Input {...register('email')} />
{errors.email && <p>{errors.email.message}</p>}
<Button type="submit" loading={isSubmitting}>
Submit
</Button>
</form>
);
}
Zod Schema Patterns
const profileSchema = z.object({
name: z.string().min(1, 'Required'),
email: z.string().email(),
website: z.string().url().optional(),
age: z.coerce.number().int().min(0),
price: z.coerce.number().positive(),
tags: z.array(z.string()).min(1),
status: z.enum(['active', 'inactive']),
});
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
import { Controller } from 'react-hook-form';
import { Select, SelectContent, SelectItem, SelectTrigger } from '@trycompai/design-system';
<Controller
name="status"
control={control}
render={({ field }) => (
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger>{field.value || 'Select...'}</SelectTrigger>
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
</SelectContent>
</Select>
)}
/>
Form State
const {
register,
handleSubmit,
control,
watch,
setValue,
reset,
setError,
formState: {
errors,
isSubmitting,
isValid,
isDirty,
},
} = useForm<FormData>({
resolver: zodResolver(schema),
mode: 'onChange',
});
Error Handling
const onSubmit = async (data: FormData) => {
try {
await submitToApi(data);
} catch (error) {
setError('email', { message: 'Email taken' });
setError('root', { message: 'Something went wrong' });
}
};
{errors.root && <p>{errors.root.message}</p>}
Dynamic Fields
import { useFieldArray } from 'react-hook-form';
const { fields, append, remove } = useFieldArray({
control,
name: 'items',
});
{fields.map((field, index) => (
<div key={field.id}>
<Input {...register(`items.${index}.name`)} />
<Button type="button" onClick={() => remove(index)}>Remove</Button>
</div>
))}
<Button type="button" onClick={() => append({ name: '' })}>Add</Button>
Anti-Patterns
const [email, setEmail] = useState('');
if (email.length < 5) setError('Too short');
<Button onClick={handleCancel}>Cancel</Button>
const { register } = useForm();
const schema = z.object({ email: z.string().min(5) });
<Button type="button" onClick={handleCancel}>Cancel</Button>