Skip to main content

forms

Use when building forms - covers React Hook Form, Zod validation, and form patterns

Source facts

Repository
trycompai/comp
Last source activity
April 27, 2026 at 13:56
Detected SKILL.md language
English
Stars
2,066
Forks
439

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
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<typeof formSchema>; // 3. Use in component 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 ```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'; <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 ```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<FormData>({ 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 && <p>{errors.root.message}</p>} ``` ## Dynamic Fields ```tsx 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 ```tsx // ❌ useState for form fields const [email, setEmail] = useState(''); // ❌ Manual validation if (email.length < 5) setError('Too short'); // ❌ Missing button type (defaults to submit) <Button onClick={handleCancel}>Cancel</Button> // ✅ Correct const { register } = useForm(); const schema = z.object({ email: z.string().min(5) }); <Button type="button" onClick={handleCancel}>Cancel</Button> ```
View on GitHub