Почему React Hook Form
React Hook Form — библиотека для работы с формами, которая минимизирует ререндеры и упрощает валидацию. В сочетании с Zod получается мощная типобезопасная система форм.
Базовая форма
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Zod schema
const loginSchema = z.object({
email: z.string().email('Введите корректный email'),
password: z.string().min(8, 'Минимум 8 символов'),
rememberMe: z.boolean().optional()
});
type LoginFormData = z.infer<typeof loginSchema>;
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting }
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
defaultValues: {
email: '',
password: '',
rememberMe: false
}
});
const onSubmit = async (data: LoginFormData) => {
console.log(data);
// API запрос
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<input
{...register('email')}
type="email"
placeholder="Email"
/>
{errors.email && (
<span className="error">{errors.email.message}</span>
)}
</div>
<div>
<input
{...register('password')}
type="password"
placeholder="Пароль"
/>
{errors.password && (
<span className="error">{errors.password.message}</span>
)}
</div>
<label>
<input {...register('rememberMe')} type="checkbox" />
Запомнить меня
</label>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Вход...' : 'Войти'}
</button>
</form>
);
}
Сложные Zod схемы
// Регистрация с подтверждением пароля
const registerSchema = z.object({
name: z.string().min(2, 'Минимум 2 символа'),
email: z.string().email('Некорректный email'),
password: z
.string()
.min(8, 'Минимум 8 символов')
.regex(/[A-Z]/, 'Нужна заглавная буква')
.regex(/[0-9]/, 'Нужна цифра'),
confirmPassword: z.string(),
age: z.number().min(18, 'Минимум 18 лет').optional(),
role: z.enum(['user', 'admin']),
terms: z.literal(true, {
errorMap: () => ({ message: 'Примите условия' })
})
}).refine((data) => data.password === data.confirmPassword, {
message: 'Пароли не совпадают',
path: ['confirmPassword']
});
// Условная валидация
const orderSchema = z.object({
deliveryType: z.enum(['pickup', 'delivery']),
address: z.string().optional()
}).refine((data) => {
if (data.deliveryType === 'delivery') {
return data.address && data.address.length > 5;
}
return true;
}, {
message: 'Укажите адрес доставки',
path: ['address']
});
// Массивы и вложенные объекты
const productSchema = z.object({
name: z.string().min(1),
variants: z.array(z.object({
size: z.string(),
price: z.number().positive(),
stock: z.number().int().min(0)
})).min(1, 'Добавьте хотя бы один вариант')
});
Контролируемые поля
import { useForm, Controller } from 'react-hook-form';
import { Select, DatePicker, Slider } from './ui';
function ProductForm() {
const { control, handleSubmit } = useForm<ProductFormData>({
resolver: zodResolver(productSchema)
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* Кастомный Select */}
<Controller
name="category"
control={control}
render={({ field, fieldState }) => (
<Select
{...field}
options={categories}
error={fieldState.error?.message}
/>
)}
/>
{/* DatePicker */}
<Controller
name="publishDate"
control={control}
render={({ field }) => (
<DatePicker
selected={field.value}
onChange={field.onChange}
/>
)}
/>
{/* Slider */}
<Controller
name="price"
control={control}
render={({ field }) => (
<Slider
value={field.value}
onChange={field.onChange}
min={0}
max={10000}
/>
)}
/>
</form>
);
}
Динамические поля (useFieldArray)
import { useForm, useFieldArray } from 'react-hook-form';
function OrderForm() {
const { register, control, handleSubmit } = useForm<OrderFormData>({
defaultValues: {
items: [{ name: '', quantity: 1 }]
}
});
const { fields, append, remove } = useFieldArray({
control,
name: 'items'
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`items.${index}.name`)} />
<input
{...register(`items.${index}.quantity`, { valueAsNumber: true })}
type="number"
/>
<button type="button" onClick={() => remove(index)}>
Удалить
</button>
</div>
))}
<button type="button" onClick={() => append({ name: '', quantity: 1 })}>
Добавить товар
</button>
<button type="submit">Оформить заказ</button>
</form>
);
}
Обработка ошибок сервера
function RegistrationForm() {
const { setError, handleSubmit } = useForm<FormData>({
resolver: zodResolver(schema)
});
const onSubmit = async (data: FormData) => {
try {
await registerUser(data);
} catch (error) {
if (error.code === 'EMAIL_EXISTS') {
setError('email', {
type: 'server',
message: 'Email уже зарегистрирован'
});
} else {
setError('root', {
type: 'server',
message: 'Произошла ошибка, попробуйте позже'
});
}
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
{errors.root && <div className="error">{errors.root.message}</div>}
{/* поля формы */}
</form>
);
}
Заключение
React Hook Form + Zod — мощная комбинация для типобезопасных форм. Минимум ререндеров, декларативная валидация, отличная поддержка TypeScript. Используйте Controller для кастомных компонентов, useFieldArray для динамических полей.
Валидация на клиенте — для UX, валидация на сервере — для безопасности. Используйте обе.
