Оптимизация форм в React: Гид по использованию React Hook Form с Zod

Вступление

Управление формами — одна из самых рутинных задач фронтенд-разработки. Ещё недавно этот процесс требовал написания огромного количества кода: отслеживание состояний полей, ручная валидация, обработка сабмитов. Современные инструменты меняют правила игры. Сегодня рассмотрим, как сочетание React Hook Form и Zod создаёт жестко типизированное решение для обработки форм с минимальной нагрузкой и максимальной надежностью.

Почему React Hook Form + Zod?

React Hook Form (RHF) решает ключевую проблему: производительность. Его построение на uncontrolled-компонентах снижает количество ререндеров до минимума. Для иллюстрации: традиционная форма с useState вызывает рендер при каждом вводе символа, RHF обрабатывает изменения без ререндеров.

Zod привносит строгие схемы валидации с TypeScript-first подходом. Он заменяет разрозненные функции проверки целостной системой с автоматическим выводом типов.

tsx
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>;

Этот небольшой фрагмент определяет:

  • Типизированную структуру данных формы
  • Правила валидации с кастомными сообщениями
  • Автоматически генерируемый тип для работы с формой

Практический пример: Создание типизированной формы

Рассмотрим создание формы входа с валидацией:

tsx
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 интегрирует схему валидации в RHF
  • register привязывает инпуты к библиотеке без обёрток
  • Ошибки привязываются к полям через объект errors

Продвинутые техники

Составные схемы и кастомная валидация

Zod позволяет создавать сложные системы валидации:

ts
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 поддерживает асинхронные проверки без дополнительных костылей:

tsx
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 уже используется" }
);

Оптимизация производительности

Для сложных форм критично контролировать ререндеры:

tsx
const { watch } = useForm();

// Отслеживание только необходимых полей
const lastName = watch("lastName");

// Мемоизация тяжелых компонентов
const AddressSection = React.useMemo(() => {
  return <ComplexAddressInput />;
}, []);

Ошибки производительности и как их избежать

  1. Избыточные рендеры: Частая ошибка — оборачивание всех инпутов в отдельные компоненты. Используйте FormProvider тогда, когда действительно необходимо:
tsx
const methods = useForm();
<FormProvider {...methods}>
  {/* Поля формы */}
</FormProvider>
  1. Некорректная типизация: Zod автоматически генерирует типы, но разработчики часто пишут дублирующие интерфейсы. Решение:
ts
// Так делать не надо!
interface UserData {
  email: string;
}

// Правильно:
type UserData = z.infer<typeof userSchema>;
  1. Злоупотребение mode валидации: RHF поддерживает несколько стратегий валидации. Молчаливой установкой является onSubmit, но можно изменить на onChange или onBlur. Оптимальный компромисс — дефолтные настройки плюс ручной триггер для критичных полей:
tsx
useForm({
  mode: "onSubmit",
  reValidateMode: "onChange",
  criteriaMode: "all"
});

Рекомендации для сложных форм

  1. Декомпозируйте большие формы на подформы с вложенными схемами:
ts
const addressSchema = z.object({
  street: z.string(),
  city: z.string()
});

const userSchema = z.object({
  personal: personalSchema,
  billingAddress: addressSchema,
  shippingAddress: addressSchema
});
  1. Для компонуемости используйте паттерн контролируемого Field Controller:
tsx
<Controller
  name="avatar"
  render={({ field }) => (
    <Uploader 
      value={field.value}
      onChange={field.onChange}
    />
  )}
/>
  1. Грамотно работайте с дефолтными значениями:
tsx
useForm({
  defaultValues: async () => {
    const data = await fetchInitialData();
    // Приведение к типу схемы
    return userSchema.parse(data); 
  }
});

Тестирование

Библиотеки существенно упрощают тестирование форм. Пример с Testing Library:

tsx
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'ы для специфических сценариев валидации

Создание форм больше не должно быть болью. Комбинация этих инструментов даёт современное решение, достойное сложных требований современных приложений.