Back to Blog
ReactFormsReact Hook FormValidation

Forms with React Hook Form

Build performant forms with React Hook Form. From basic usage to validation to complex patterns.

B
Bootspring Team
Engineering
July 3, 2021
6 min read

React Hook Form provides performant, flexible forms. Here's how to use it effectively.

Basic Setup#

1import { useForm } from 'react-hook-form'; 2 3interface FormData { 4 email: string; 5 password: string; 6} 7 8function LoginForm() { 9 const { 10 register, 11 handleSubmit, 12 formState: { errors, isSubmitting }, 13 } = useForm<FormData>(); 14 15 const onSubmit = async (data: FormData) => { 16 await login(data); 17 }; 18 19 return ( 20 <form onSubmit={handleSubmit(onSubmit)}> 21 <input 22 {...register('email', { required: 'Email is required' })} 23 type="email" 24 placeholder="Email" 25 /> 26 {errors.email && <span>{errors.email.message}</span>} 27 28 <input 29 {...register('password', { required: 'Password is required' })} 30 type="password" 31 placeholder="Password" 32 /> 33 {errors.password && <span>{errors.password.message}</span>} 34 35 <button type="submit" disabled={isSubmitting}> 36 {isSubmitting ? 'Loading...' : 'Login'} 37 </button> 38 </form> 39 ); 40}

Validation Rules#

1interface FormData { 2 username: string; 3 email: string; 4 password: string; 5 confirmPassword: string; 6 age: number; 7 website: string; 8} 9 10function RegistrationForm() { 11 const { register, handleSubmit, watch, formState: { errors } } = useForm<FormData>(); 12 13 const password = watch('password'); 14 15 return ( 16 <form onSubmit={handleSubmit(onSubmit)}> 17 <input 18 {...register('username', { 19 required: 'Username is required', 20 minLength: { value: 3, message: 'Min 3 characters' }, 21 maxLength: { value: 20, message: 'Max 20 characters' }, 22 pattern: { 23 value: /^[a-zA-Z0-9_]+$/, 24 message: 'Only letters, numbers, and underscores', 25 }, 26 })} 27 /> 28 29 <input 30 {...register('email', { 31 required: 'Email is required', 32 pattern: { 33 value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i, 34 message: 'Invalid email address', 35 }, 36 })} 37 /> 38 39 <input 40 {...register('password', { 41 required: 'Password is required', 42 minLength: { value: 8, message: 'Min 8 characters' }, 43 validate: { 44 hasUpperCase: (v) => 45 /[A-Z]/.test(v) || 'Must contain uppercase', 46 hasNumber: (v) => 47 /\d/.test(v) || 'Must contain number', 48 }, 49 })} 50 type="password" 51 /> 52 53 <input 54 {...register('confirmPassword', { 55 required: 'Please confirm password', 56 validate: (v) => v === password || 'Passwords do not match', 57 })} 58 type="password" 59 /> 60 61 <input 62 {...register('age', { 63 required: 'Age is required', 64 valueAsNumber: true, 65 min: { value: 18, message: 'Must be 18 or older' }, 66 max: { value: 120, message: 'Invalid age' }, 67 })} 68 type="number" 69 /> 70 71 <button type="submit">Register</button> 72 </form> 73 ); 74}

Zod Integration#

1import { useForm } from 'react-hook-form'; 2import { zodResolver } from '@hookform/resolvers/zod'; 3import { z } from 'zod'; 4 5const schema = z.object({ 6 email: z.string().email('Invalid email'), 7 password: z.string().min(8, 'Min 8 characters'), 8 role: z.enum(['admin', 'user', 'guest']), 9 preferences: z.object({ 10 newsletter: z.boolean(), 11 notifications: z.boolean(), 12 }), 13}); 14 15type FormData = z.infer<typeof schema>; 16 17function Form() { 18 const { 19 register, 20 handleSubmit, 21 formState: { errors }, 22 } = useForm<FormData>({ 23 resolver: zodResolver(schema), 24 defaultValues: { 25 preferences: { 26 newsletter: false, 27 notifications: true, 28 }, 29 }, 30 }); 31 32 return ( 33 <form onSubmit={handleSubmit(onSubmit)}> 34 <input {...register('email')} /> 35 {errors.email && <span>{errors.email.message}</span>} 36 37 <input {...register('password')} type="password" /> 38 39 <select {...register('role')}> 40 <option value="user">User</option> 41 <option value="admin">Admin</option> 42 <option value="guest">Guest</option> 43 </select> 44 45 <label> 46 <input {...register('preferences.newsletter')} type="checkbox" /> 47 Newsletter 48 </label> 49 50 <button type="submit">Submit</button> 51 </form> 52 ); 53}

Controller for Custom Components#

1import { useForm, Controller } from 'react-hook-form'; 2import Select from 'react-select'; 3import DatePicker from 'react-datepicker'; 4 5interface FormData { 6 country: { value: string; label: string }; 7 birthDate: Date; 8 rating: number; 9} 10 11function CustomForm() { 12 const { control, handleSubmit } = useForm<FormData>(); 13 14 return ( 15 <form onSubmit={handleSubmit(onSubmit)}> 16 <Controller 17 name="country" 18 control={control} 19 rules={{ required: 'Country is required' }} 20 render={({ field, fieldState: { error } }) => ( 21 <> 22 <Select 23 {...field} 24 options={countries} 25 placeholder="Select country" 26 /> 27 {error && <span>{error.message}</span>} 28 </> 29 )} 30 /> 31 32 <Controller 33 name="birthDate" 34 control={control} 35 rules={{ required: 'Birth date is required' }} 36 render={({ field }) => ( 37 <DatePicker 38 selected={field.value} 39 onChange={field.onChange} 40 placeholderText="Select date" 41 /> 42 )} 43 /> 44 45 <Controller 46 name="rating" 47 control={control} 48 defaultValue={3} 49 render={({ field }) => ( 50 <input 51 type="range" 52 min={1} 53 max={5} 54 {...field} 55 onChange={(e) => field.onChange(parseInt(e.target.value))} 56 /> 57 )} 58 /> 59 60 <button type="submit">Submit</button> 61 </form> 62 ); 63}

Form Arrays#

1import { useForm, useFieldArray } from 'react-hook-form'; 2 3interface FormData { 4 users: Array<{ 5 name: string; 6 email: string; 7 }>; 8} 9 10function DynamicForm() { 11 const { register, control, handleSubmit } = useForm<FormData>({ 12 defaultValues: { 13 users: [{ name: '', email: '' }], 14 }, 15 }); 16 17 const { fields, append, remove, move } = useFieldArray({ 18 control, 19 name: 'users', 20 }); 21 22 return ( 23 <form onSubmit={handleSubmit(onSubmit)}> 24 {fields.map((field, index) => ( 25 <div key={field.id}> 26 <input 27 {...register(`users.${index}.name`, { required: true })} 28 placeholder="Name" 29 /> 30 <input 31 {...register(`users.${index}.email`, { required: true })} 32 placeholder="Email" 33 /> 34 <button type="button" onClick={() => remove(index)}> 35 Remove 36 </button> 37 <button type="button" onClick={() => move(index, index - 1)}> 38 Up 39 </button> 40 </div> 41 ))} 42 43 <button type="button" onClick={() => append({ name: '', email: '' })}> 44 Add User 45 </button> 46 47 <button type="submit">Submit</button> 48 </form> 49 ); 50}

Form State and Methods#

1function AdvancedForm() { 2 const { 3 register, 4 handleSubmit, 5 watch, 6 setValue, 7 getValues, 8 reset, 9 trigger, 10 setError, 11 clearErrors, 12 formState: { 13 errors, 14 isSubmitting, 15 isValid, 16 isDirty, 17 dirtyFields, 18 touchedFields, 19 }, 20 } = useForm<FormData>({ 21 mode: 'onChange', // Validate on change 22 defaultValues: { 23 email: '', 24 subscribe: false, 25 }, 26 }); 27 28 // Watch specific field 29 const subscribe = watch('subscribe'); 30 31 // Watch all fields 32 const allValues = watch(); 33 34 // Set value programmatically 35 const fillDemoData = () => { 36 setValue('email', 'demo@example.com', { 37 shouldValidate: true, 38 shouldDirty: true, 39 }); 40 }; 41 42 // Get values 43 const handleCheck = () => { 44 const values = getValues(); 45 console.log(values); 46 }; 47 48 // Reset form 49 const handleReset = () => { 50 reset(); // Reset to defaultValues 51 // Or reset to specific values 52 reset({ email: 'new@example.com' }); 53 }; 54 55 // Trigger validation 56 const validateEmail = async () => { 57 const result = await trigger('email'); 58 console.log('Valid:', result); 59 }; 60 61 // Set custom error 62 const handleServerError = () => { 63 setError('email', { 64 type: 'server', 65 message: 'Email already exists', 66 }); 67 }; 68 69 return ( 70 <form onSubmit={handleSubmit(onSubmit)}> 71 <input {...register('email')} /> 72 73 <label> 74 <input {...register('subscribe')} type="checkbox" /> 75 Subscribe 76 </label> 77 78 {subscribe && ( 79 <input {...register('frequency')} placeholder="Frequency" /> 80 )} 81 82 <button type="submit" disabled={!isValid || isSubmitting}> 83 Submit 84 </button> 85 86 <button type="button" onClick={handleReset}> 87 Reset 88 </button> 89 </form> 90 ); 91}

Reusable Form Components#

1// Input component 2interface InputProps { 3 name: string; 4 label: string; 5 register: UseFormRegister<any>; 6 error?: FieldError; 7 rules?: RegisterOptions; 8 type?: string; 9} 10 11function Input({ name, label, register, error, rules, type = 'text' }: InputProps) { 12 return ( 13 <div className="form-group"> 14 <label htmlFor={name}>{label}</label> 15 <input 16 id={name} 17 type={type} 18 {...register(name, rules)} 19 className={error ? 'error' : ''} 20 /> 21 {error && <span className="error-message">{error.message}</span>} 22 </div> 23 ); 24} 25 26// Usage 27function MyForm() { 28 const { register, formState: { errors } } = useForm(); 29 30 return ( 31 <form> 32 <Input 33 name="email" 34 label="Email" 35 register={register} 36 error={errors.email} 37 rules={{ required: 'Email required' }} 38 /> 39 </form> 40 ); 41}

Server-Side Validation#

1function FormWithServerValidation() { 2 const { 3 register, 4 handleSubmit, 5 setError, 6 formState: { errors }, 7 } = useForm<FormData>(); 8 9 const onSubmit = async (data: FormData) => { 10 try { 11 await api.register(data); 12 } catch (error) { 13 if (error.response?.status === 400) { 14 const serverErrors = error.response.data.errors; 15 16 Object.entries(serverErrors).forEach(([field, message]) => { 17 setError(field as keyof FormData, { 18 type: 'server', 19 message: message as string, 20 }); 21 }); 22 } 23 } 24 }; 25 26 return ( 27 <form onSubmit={handleSubmit(onSubmit)}> 28 <input {...register('email')} /> 29 {errors.email && <span>{errors.email.message}</span>} 30 {/* ... */} 31 </form> 32 ); 33}

Best Practices#

Performance: ✓ Use uncontrolled inputs (register) ✓ Isolate re-renders with Controller ✓ Use mode: 'onBlur' for large forms ✓ Avoid watching entire form Validation: ✓ Use Zod for complex schemas ✓ Show errors on blur or submit ✓ Validate on server too ✓ Provide clear error messages Structure: ✓ Create reusable input components ✓ Type forms with interfaces ✓ Use defaultValues ✓ Handle loading and error states

Conclusion#

React Hook Form provides excellent performance through uncontrolled inputs while maintaining flexibility. Use Zod for schema validation, Controller for custom components, and useFieldArray for dynamic lists. The minimal re-render approach makes it ideal for complex forms.

Share this article

Help spread the word about Bootspring