Вступление
Управление формами — одна из самых рутинных задач фронтенд-разработки. Ещё недавно этот процесс требовал написания огромного количества кода: отслеживание состояний полей, ручная валидация, обработка сабмитов. Современные инструменты меняют правила игры. Сегодня рассмотрим, как сочетание React Hook Form и Zod создаёт жестко типизированное решение для обработки форм с минимальной нагрузкой и максимальной надежностью.
Почему React Hook Form + Zod?
React Hook Form (RHF) решает ключевую проблему: производительность. Его построение на uncontrolled-компонентах снижает количество ререндеров до минимума. Для иллюстрации: традиционная форма с useState вызывает рендер при каждом вводе символа, RHF обрабатывает изменения без ререндеров.
Zod привносит строгие схемы валидации с TypeScript-first подходом. Он заменяет разрозненные функции проверки целостной системой с автоматическим выводом типов.
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
const userSchema = z.object({
email: z.string().email("Некорректный email"),
password: z.string().min(8, "Не менее 8 символов"),
age: z.number().min(18, "18+ только")
});
type UserFormData = z.infer<typeof userSchema>;
Этот небольшой фрагмент определяет:
- Типизированную структуру данных формы
- Правила валидации с кастомными сообщениями
- Автоматически генерируемый тип для работы с формой
Практический пример: Создание типизированной формы
Рассмотрим создание формы входа с валидацией:
import { useForm } from "react-hook-form";
const LoginForm = () => {
const { register, handleSubmit, formState: { errors } } = useForm<UserFormData>({
resolver: zodResolver(userSchema)
});
const onSubmit = (data: UserFormData) => {
console.log("Данные валидны:", data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label>Email</label>
<input {...register("email")} />
{errors.email && <span>{errors.email.message}</span>}
</div>
<div>
<label>Пароль</label>
<input type="password" {...register("password")} />
{errors.password && <span>{errors.password.message}</span>}
</div>
<button type="submit">Войти</button>
</form>
);
};
Ключевые моменты:
zodResolver
интегрирует схему валидации в RHFregister
привязывает инпуты к библиотеке без обёрток- Ошибки привязываются к полям через объект
errors
Продвинутые техники
Составные схемы и кастомная валидация
Zod позволяет создавать сложные системы валидации:
const passwordSchema = z.string()
.min(8)
.refine(pwd => /[A-Z]/.test(pwd), "Должен содержать заглавную букву")
.refine(pwd => /\d/.test(pwd), "Должен содержать цифру");
const profileSchema = userSchema.extend({
bio: z.string().max(500).optional(),
website: z.string().url().optional(),
socials: z.array(z.string().url()),
birthDate: z.preprocess(
arg => typeof arg === "string" ? new Date(arg) : arg,
z.date().min(new Date("1900-01-01"))
)
});
Асинхронная валидация
RHF поддерживает асинхронные проверки без дополнительных костылей:
const asyncEmailValidation = z.string().email().refine(
async email => {
const res = await fetch(`/api/check-email?email=${email}`);
const { available } = await res.json();
return available;
},
{ message: "Email уже используется" }
);
Оптимизация производительности
Для сложных форм критично контролировать ререндеры:
const { watch } = useForm();
// Отслеживание только необходимых полей
const lastName = watch("lastName");
// Мемоизация тяжелых компонентов
const AddressSection = React.useMemo(() => {
return <ComplexAddressInput />;
}, []);
Ошибки производительности и как их избежать
- Избыточные рендеры: Частая ошибка — оборачивание всех инпутов в отдельные компоненты. Используйте
FormProvider
тогда, когда действительно необходимо:
const methods = useForm();
<FormProvider {...methods}>
{/* Поля формы */}
</FormProvider>
- Некорректная типизация: Zod автоматически генерирует типы, но разработчики часто пишут дублирующие интерфейсы. Решение:
// Так делать не надо!
interface UserData {
email: string;
}
// Правильно:
type UserData = z.infer<typeof userSchema>;
- Злоупотребение mode валидации: RHF поддерживает несколько стратегий валидации. Молчаливой установкой является
onSubmit
, но можно изменить наonChange
илиonBlur
. Оптимальный компромисс — дефолтные настройки плюс ручной триггер для критичных полей:
useForm({
mode: "onSubmit",
reValidateMode: "onChange",
criteriaMode: "all"
});
Рекомендации для сложных форм
- Декомпозируйте большие формы на подформы с вложенными схемами:
const addressSchema = z.object({
street: z.string(),
city: z.string()
});
const userSchema = z.object({
personal: personalSchema,
billingAddress: addressSchema,
shippingAddress: addressSchema
});
- Для компонуемости используйте паттерн контролируемого Field Controller:
<Controller
name="avatar"
render={({ field }) => (
<Uploader
value={field.value}
onChange={field.onChange}
/>
)}
/>
- Грамотно работайте с дефолтными значениями:
useForm({
defaultValues: async () => {
const data = await fetchInitialData();
// Приведение к типу схемы
return userSchema.parse(data);
}
});
Тестирование
Библиотеки существенно упрощают тестирование форм. Пример с Testing Library:
test("Отображает ошибку валидации", async () => {
render(<LoginForm />);
fireEvent.input(screen.getByLabelText("Email"), {
target: { value: "неправильный-email" }
});
fireEvent.submit(screen.getByText("Войти"));
expect(await screen.findByText("Некорректный email")).toBeVisible();
});
Заключение
Фронтенд сильно изменился за последние годы, и подходы к работе с формами — особенно. Сочетание React Hook Form и Zod представляет собой не просто удобную связку инструментов, а законченную методологию:
- Минимальная нагрузка на производительность благодаря архитектуре RHF
- Надёжная типизация через схемы Zod
- Быстрое развитие от простых форм к сложным архитектурам
- Объектно-ориентированный подход к валидации
Для углубления в тему рекомендую исследовать:
- Интеграцию с Formik для миграции существующих проектов
- Библиотеку
rhf-zod-server-resolver
для применение схем на бэкенде - Кастомные resolver'ы для специфических сценариев валидации
Создание форм больше не должно быть болью. Комбинация этих инструментов даёт современное решение, достойное сложных требований современных приложений.