Интеграция с React Hook Form

Интеграция библиотеки валидации Yup с формами на основе React Hook Form строится вокруг идеи разделения ответственности: React Hook Form управляет состоянием и жизненным циклом формы, а Yup отвечает за декларативное описание и выполнение правил валидации.

Основной механизм связывания этих двух инструментов реализуется через резолверы (resolvers). Они выступают адаптером между схемой валидации и системой регистрации полей формы, преобразуя результаты проверки Yup в формат, понятный React Hook Form.


Ключевой точкой интеграции является пакет @hookform/resolvers. Он предоставляет функцию yupResolver, которая преобразует схему Yup в формат, используемый React Hook Form.

Типовой сценарий:

import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";

Далее создаётся схема:

const schema = yup.object({
  email: yup.string().email("Некорректный email").required("Обязательное поле"),
  password: yup.string().min(8, "Минимум 8 символов").required(),
});

И подключается к форме:

const {
  register,
  handleSubmit,
  formState: { errors },
} = useForm({
  resolver: yupResolver(schema),
});

На этом уровне происходит ключевая интеграция: React Hook Form больше не выполняет встроенную валидацию, а делегирует её Yup через resolver.


Архитектурная модель взаимодействия

Интеграция строится по следующему принципу:

  • React Hook Form фиксирует изменения полей и триггеры валидации
  • Yup получает полное или частичное значение формы
  • Yup выполняет синхронную или асинхронную проверку
  • результат преобразуется resolver’ом в структуру ошибок

Важно, что Yup возвращает объект ошибок, который затем нормализуется:

{
  email: { message: "Некорректный email", type: "validation" }
}

Эта структура напрямую используется React Hook Form для отображения состояния ошибок.


Работа с типами и TypeScript

При использовании TypeScript схема Yup может быть источником типов формы:

import * as yup from "yup";

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

type FormData = yup.InferType<typeof schema>;

Далее тип передаётся в useForm:

const { register } = useForm<FormData>({
  resolver: yupResolver(schema),
});

Такой подход исключает дублирование типов между UI и валидацией.


Механика валидации: когда вызывается Yup

React Hook Form вызывает resolver в следующих ситуациях:

  • submit формы (handleSubmit)
  • изменение поля (в зависимости от mode)
  • потеря фокуса (onBlur)
  • ручной вызов trigger

Yup при этом может выполнять:

  • синхронную валидацию (по умолчанию)
  • асинхронную валидацию (например, проверка уникальности через API)

Пример асинхронного правила:

username: yup.string().test(
  "check-username",
  "Имя занято",
  async (value) => {
    const res = await fetch(`/api/check?u=${value}`);
    const data = await res.json();
    return data.available;
  }
);

Поведение при частичной валидации

React Hook Form не всегда отправляет всю форму в Yup. В зависимости от события может передаваться:

  • вся форма (submit)
  • одно поле (trigger / onChange)

Yup при этом должен корректно обрабатывать partial validation. Для этого важно использовать abortEarly: false:

yupResolver(schema, { abortEarly: false });

Это позволяет собрать все ошибки за один проход, а не останавливаться на первой.


Глубоко вложенные структуры

Yup поддерживает вложенные объекты и массивы, что напрямую транслируется в структуру формы React Hook Form.

const schema = yup.object({
  user: yup.object({
    profile: yup.object({
      firstName: yup.string().required(),
    }),
  }),
});

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

register("user.profile.firstName");

React Hook Form автоматически сопоставляет вложенные ключи с результатами Yup.


Работа с массивами

Особенно важный кейс — динамические поля:

const schema = yup.object({
  items: yup.array().of(
    yup.object({
      title: yup.string().required(),
    })
  ),
});

В React Hook Form:

  • используется useFieldArray
  • Yup валидирует каждый элемент массива

Ошибки возвращаются с индексной структурой:

items[0].title
items[1].title

Контроль режима валидации

Интеграция с Yup сильно зависит от режима React Hook Form:

  • onSubmit — минимальная нагрузка, проверка только при отправке
  • onChange — постоянная валидация
  • onBlur — баланс между UX и производительностью
useForm({
  resolver: yupResolver(schema),
  mode: "onBlur",
});

При частой валидации важно учитывать стоимость Yup-схем, особенно при больших формах.


Динамическая валидация и context

Yup поддерживает context, который может использоваться для динамических условий:

const schema = yup.object({
  password: yup.string().required(),
  confirmPassword: yup.string().oneOf(
    [yup.ref("password")],
    "Пароли не совпадают"
  ),
});

React Hook Form позволяет передавать context:

useForm({
  resolver: yupResolver(schema),
  context: { mode: "register" },
});

Это используется для условной логики в сложных формах.


Управление ошибками

Ошибки, возвращаемые через resolver, попадают в:

formState.errors

Структура ошибок соответствует полям формы:

errors.email?.message

Важная особенность: React Hook Form не хранит ошибки Yup напрямую, а только нормализованную версию, что снижает связанность систем.


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

При больших формах с десятками полей Yup может стать узким местом. Основные оптимизации:

  • использование abortEarly: false
  • разделение схем на модули
  • мемоизация схемы через useMemo
  • избежание пересоздания schema на каждом рендере
const schema = useMemo(() => createSchema(), []);

Декомпозиция схем

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

const addressSchema = yup.object({
  city: yup.string().required(),
  zip: yup.string().required(),
});

const userSchema = yup.object({
  name: yup.string().required(),
  address: addressSchema,
});

Такой подход позволяет переиспользовать части валидации между формами.


Взаимодействие с Controller

Для сложных компонентов (select, datepicker) используется Controller:

<Controller
  name="email"
  control={control}
  render={({ field }) => <input {...field} />}
/>

Yup при этом остаётся полностью независимым от UI-слоя, что сохраняет чистоту архитектуры: схема описывает данные, а не компоненты.


Программная триггеризация валидации

React Hook Form позволяет запускать Yup вручную:

trigger("email");

или

trigger(["email", "password"]);

Это приводит к повторному запуску resolver и повторной проверке Yup.


Кастомные ошибки и расширение Yup

Yup позволяет расширять поведение через test:

yup.addMethod(yup.string, "noSpaces", function (message) {
  return this.test("no-spaces", message, (value) => !/\s/.test(value));
});

Это делает возможным построение доменно-ориентированных правил валидации, не выходя за пределы схемы.


Синхронизация состояния формы и схемы

Главное архитектурное свойство интеграции — отсутствие дублирования состояния:

  • React Hook Form хранит значения и touched/dirty состояния
  • Yup не хранит состояние, работает как чистая функция
  • resolver связывает их в точке вызова

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