Работа с React Hook Form

Связка схемной валидации и форменной логики строится через resolver-слой, который преобразует правила проверки Zod в формат, понятный системе управления формами. В экосистеме React Hook Form это реализуется через пакет @hookform/resolvers/zod, обеспечивающий синхронизацию состояния формы и результатов валидации.

Основной принцип архитектуры заключается в разделении ответственности: React Hook Form управляет состоянием и подписками на поля, Zod описывает строгую структуру данных и правила их проверки.


Базовая схема и подключение resolver

Определение схемы строится декларативно:

import { z } from "zod";

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});

Интеграция с формой:

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

const form = useForm({
  resolver: zodResolver(schema),
});

Валидация начинает выполняться на уровне resolver-а, а React Hook Form получает уже нормализованные ошибки.


Типизация данных через Zod

Одним из ключевых преимуществ является автоматическое выведение типов:

type FormData = z.infer<typeof schema>;

Эта конструкция устраняет дублирование интерфейсов и схем. Типы и правила становятся единым источником истины.


Регистрация полей и связь с моделью данных

Каждое поле связывается с системой через register:

const { register, handleSubmit, formState: { errors } } = form;

<input {...register("email")} />
<input {...register("password")} />

Ошибки, возвращаемые Zod, автоматически отображаются в formState.errors:

{errors.email?.message}
{errors.password?.message}

Структурированные объекты и вложенные схемы

Zod поддерживает вложенные структуры, которые напрямую отображаются в форме:

const schema = z.object({
  user: z.object({
    name: z.string(),
    age: z.number().min(18),
  }),
});

Использование в форме:

<input {...register("user.name")} />
<input {...register("user.age", { valueAsNumber: true })} />

Ключевой момент заключается в синхронизации точечной нотации React Hook Form с деревом объектов Zod.


Массивы и динамические поля

Работа с коллекциями реализуется через useFieldArray:

import { useFieldArray } from "react-hook-form";

const { fields, append, remove } = useFieldArray({
  control,
  name: "items",
});

Схема:

const schema = z.object({
  items: z.array(
    z.object({
      title: z.string(),
      quantity: z.number(),
    })
  ),
});

Каждый элемент массива валидируется независимо, но в рамках общей структуры.


Контроль сложных компонентов

Для нестандартных компонентов используется Controller:

import { Controller } from "react-hook-form";

<Controller
  name="category"
  control={control}
  render={({ field }) => (
    <Select {...field} />
  )}
/>

Zod продолжает контролировать тип и ограничения, несмотря на абстракцию UI-компонента.


Трансформации и приведение данных

Zod позволяет изменять данные до попадания в форму результата:

const schema = z.object({
  price: z.string().transform((val) => Number(val)),
});

Такой подход устраняет необходимость ручного парсинга данных после submit.


Уточнённые проверки через refine

Сложные бизнес-правила реализуются через refine:

const schema = z.object({
  password: z.string(),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  path: ["confirmPassword"],
  message: "Пароли не совпадают",
});

Ошибки привязываются к конкретному полю, сохраняя консистентность UI.


Асинхронная валидация

Несмотря на синхронную природу Zod, React Hook Form допускает асинхронные проверки:

const schema = z.object({
  username: z.string().refine(async (value) => {
    const res = await fetch(`/api/check?u=${value}`);
    return res.ok;
  }, {
    message: "Имя занято",
  }),
});

Такая логика увеличивает нагрузку, поэтому часто комбинируется с debounce на уровне UI.


Режимы валидации и поведение формы

React Hook Form поддерживает несколько стратегий:

  • onSubmit
  • onChange
  • onBlur
  • all

Пример конфигурации:

useForm({
  resolver: zodResolver(schema),
  mode: "onBlur",
});

Выбор режима влияет на момент запуска схемной проверки Zod.


Повторное использование схем

Схемы Zod допускают композицию:

const baseUser = z.object({
  email: z.string().email(),
});

const extendedUser = baseUser.extend({
  role: z.string(),
});

Такой подход снижает дублирование логики между формами.


Условная валидация

Зависимость полей выражается через superRefine:

const schema = z.object({
  hasDiscount: z.boolean(),
  discountCode: z.string().optional(),
}).superRefine((data, ctx) => {
  if (data.hasDiscount && !data.discountCode) {
    ctx.addIssue({
      path: ["discountCode"],
      message: "Требуется код скидки",
    });
  }
});

Сопоставление значений формы и схемы

Одной из ключевых проблем является несовпадение типов HTML input и строгой типизации Zod. Решается через:

  • valueAsNumber
  • valueAsDate
  • трансформации Zod
  • Controller для кастомных компонентов

Поведение ошибок и их структура

Ошибки Zod структурированы и глубоко вложены:

errors.user?.name?.message

React Hook Form сохраняет иерархию, соответствующую схеме, без потери контекста.


Оптимизация перерендера

Связка с Zod не влияет напрямую на перерисовки, но влияет на частоту валидаций. Оптимизация достигается через:

  • shouldUnregister
  • useFormState
  • изоляцию контролируемых компонентов
  • минимизацию mode: "onChange"

Моделирование доменной логики

Схемы Zod часто становятся представлением доменной модели:

const OrderSchema = z.object({
  items: z.array(
    z.object({
      id: z.string(),
      quantity: z.number().min(1),
    })
  ),
  total: z.number(),
});

React Hook Form в таком подходе выступает слоем взаимодействия, не содержащим бизнес-логики.