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.