React Hook Form
Build type-safe, validated forms using React Hook Form, Zod, and Drivn components.
Preview
A complete login form with schema validation, error handling, and loading state. Submit with empty fields to see validation in action.
Installation
Install React Hook Form, the Zod resolver, and Zod.
1 npm install react-hook-form @hookform/resolvers zod
Usage
Every form follows three steps — define a schema, call useForm, and render your fields.
1 import { useForm, type SubmitHandler } from 'react-hook-form' 2 import { zodResolver } from '@hookform/resolvers/zod' 3 import { z } from 'zod' 4 5 // 1. Define your schema 6 const schema = z.object({ 7 email: z.email('Enter a valid email'), 8 password: z.string().min(8, 'At least 8 characters'), 9 }) 10 11 type FormData = z.infer<typeof schema> 12 13 export function MyForm() { 14 // 2. Call useForm with your schema 15 const { 16 register, 17 handleSubmit, 18 formState: { errors, isSubmitting }, 19 } = useForm<FormData>({ 20 resolver: zodResolver(schema), 21 }) 22 23 const onSubmit: SubmitHandler<FormData> = async (data) => { 24 // Submit to your API 25 } 26 27 // 3. Render your fields 28 return ( 29 <form onSubmit={handleSubmit(onSubmit)}> 30 <Input {...register('email')} /> 31 {errors.email && <p>{errors.email.message}</p>} 32 33 <Input {...register('password')} /> 34 {errors.password && <p>{errors.password.message}</p>} 35 36 <Button type="submit" loading={isSubmitting}> 37 Submit 38 </Button> 39 </form> 40 ) 41 }
Use register() for components with native change events (Input, Textarea, Checkbox). Use Controller for components with custom value APIs (Select, RadioGroup, Switch, Slider, DatePicker).
Anatomy
Two patterns for connecting Drivn components to your form.
1 // register() — for Input, Textarea, Checkbox 2 // These use forwardRef + native change events 3 4 <Input 5 {...register('email')} 6 placeholder="you@example.com" 7 />
Components with native onChange events accept register() directly. Components with custom value signatures need Controller to bridge the gap.
1 // Controller — for Select, RadioGroup, Switch, Slider, DatePicker 2 // These have custom value/onChange signatures 3 4 <Controller 5 name="role" 6 control={control} 7 render={({ field }) => ( 8 <Select value={field.value} onChange={field.onChange}> 9 <Select.Trigger placeholder="Pick a role" /> 10 <Select.Menu> 11 <Select.Option value="dev">Developer</Select.Option> 12 </Select.Menu> 13 </Select> 14 )} 15 />
Profile Form
A complex form combining register() and Controller across all Drivn form components.
Input
Input uses forwardRef and native events — spread register() directly.
Textarea
Like Input, Textarea supports register() with no Controller needed.
Checkbox
Checkbox forwards its ref and fires a native change event. register() maps the boolean checked value to your schema.
Select
Select uses a custom onChange that receives a value string. Controller bridges field.value and field.onChange to the component.
Radio Group
RadioGroup fires onValueChange instead of onChange. Controller adapts this to React Hook Form's interface.
Switch
Switch onChange receives a boolean directly. Controller maps field.value to checked and field.onChange to the callback.
Slider
Slider onChange receives a number value. Controller connects field.value and field.onChange directly.
Date Picker
DatePicker uses selected and onSelect props. Controller maps field.value to selected and field.onChange to onSelect.
Zod Schema
Common Zod v4 patterns for form schemas. Zod infers TypeScript types automatically — no manual type definitions needed.
1 import { z } from 'zod' 2 3 const schema = z.object({ 4 // Top-level validators (Zod v4) 5 email: z.email('Enter a valid email'), 6 website: z.url('Enter a valid URL'), 7 id: z.uuid('Invalid ID format'), 8 9 // String with constraints 10 name: z.string().min(2, 'At least 2 characters'), 11 bio: z.string().max(200, 'Max 200 characters'), 12 13 // Enum 14 role: z.enum(['developer', 'designer', 'manager']), 15 16 // Boolean with validation 17 terms: z.boolean().refine((v) => v, 'You must accept'), 18 19 // Number (coerce from string input) 20 age: z.coerce.number().min(18).max(120), 21 22 // Date 23 startDate: z.date({ error: 'Pick a date' }), 24 }) 25 26 // Infer TypeScript type from schema 27 type FormData = z.infer<typeof schema> 28 29 // Cross-field validation 30 const passwordSchema = z 31 .object({ 32 password: z.string().min(8), 33 confirm: z.string(), 34 }) 35 .refine((d) => d.password === d.confirm, { 36 message: 'Passwords must match', 37 path: ['confirm'], 38 })
Error Messages
Display validation errors inline below each field. Zod provides the message, you just render it conditionally.
1 {errors.email && ( 2 <p className="mt-1 text-sm text-destructive"> 3 {errors.email.message} 4 </p> 5 )} 6 7 // Or with Input's built-in error prop (if supported) 8 <Input 9 {...register('email')} 10 error={errors.email?.message} 11 />