Cross-field валидация

Cross-field валидация в схемах Yup строится вокруг идеи согласованности нескольких полей внутри одной формы, когда значение одного поля зависит от другого. В связке с @hookform/resolvers/yup это становится ключевым механизмом контроля целостности данных на уровне схемы, а не на уровне UI-компонентов.

Основная сложность cross-field логики заключается в том, что простые валидаторы работают изолированно: каждое поле проверяется независимо. Однако реальные формы почти всегда содержат зависимости: подтверждение пароля, диапазоны дат, условные обязательные поля, согласованные числовые ограничения. Именно здесь Yup предоставляет инструменты ref, when, test, а YupResolver обеспечивает их интеграцию с React Hook Form без необходимости ручного управления состоянием ошибок.


Yup реализует схемную валидацию, где каждое поле описывается как часть общего объекта. Это позволяет обращаться к другим полям через контекст текущего объекта значения.

Ключевой механизм — ссылки на другие поля через Yup.ref.

import * as yup from "yup";

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

Здесь confirmPassword не существует в изоляции — его валидность определяется значением password. Это базовый паттерн cross-field проверки.

Важно понимать: yup.ref не копирует значение, а создаёт динамическую ссылку, которая разрешается во время выполнения валидации.


Механизм работы YupResolver в cross-field сценариях

YupResolver выполняет трансляцию схемы Yup в формат ошибок React Hook Form. При этом он:

  • передаёт весь объект формы в Yup как единый контекст
  • инициирует повторную валидацию зависимых полей при изменении любого значения
  • нормализует ошибки в структуру field.errors

Это означает, что cross-field зависимости автоматически учитываются без дополнительной логики синхронизации.

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

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

С точки зрения архитектуры, YupResolver выступает мостом между декларативной схемой и событийной моделью формы.


Сравнение ref и when в cross-field логике

yup.ref используется для прямого сравнения значений. Однако более сложные зависимости реализуются через when, который позволяет менять правила валидации динамически.

Прямое сравнение через ref

confirmPassword: yup
  .string()
  .oneOf([yup.ref("password")], "Пароли должны совпадать")

Используется при строгом равенстве значений.


Условная логика через when

age: yup.number().required(),

guardianConsent: yup.string().when("age", (age, schema) => {
  if (age < 18) {
    return schema.required("Требуется согласие опекуна");
  }
  return schema.notRequired();
});

Здесь guardianConsent зависит от значения age, что формирует условную структуру схемы.

when может принимать массив зависимостей:

field: yup.string().when(["a", "b"], (a, b, schema) => {
  if (a && b) {
    return schema.required();
  }
  return schema;
});

Диапазоны значений: классический cross-field сценарий

Одним из наиболее частых кейсов является проверка диапазона дат или чисел.

const schema = yup.object({
  startDate: yup.date().required(),
  endDate: yup
    .date()
    .required()
    .min(yup.ref("startDate"), "Дата окончания не может быть раньше начала")
});

Здесь min(yup.ref(...)) создаёт зависимость, при которой endDate валидируется относительно startDate.

Аналогичный подход применяется для числовых диапазонов:

minPrice: yup.number().required(),
maxPrice: yup
  .number()
  .required()
  .moreThan(yup.ref("minPrice"), "Максимум должен быть больше минимума")

Использование test для сложной cross-field логики

Когда стандартные методы Yup недостаточны, применяется test, который предоставляет доступ к полному объекту значения через this.parent.

const schema = yup.object({
  password: yup.string().required(),
  confirmPassword: yup.string().test(
    "match",
    "Пароли не совпадают",
    function (value) {
      return value === this.parent.password;
    }
  )
});

this.parent — ключевой механизм доступа к другим полям на уровне одного объекта формы.


Контекст выполнения и ограничения this.parent

this.parent работает только в пределах одного уровня объекта. При вложенных структурах необходимо учитывать путь поля.

const schema = yup.object({
  user: yup.object({
    password: yup.string(),
    confirmPassword: yup.string().test(
      "match",
      "Ошибка",
      function (value) {
        return value === this.parent.password;
      }
    )
  })
});

При глубокой вложенности важно помнить, что parent относится к ближайшему объекту, а не к корню схемы.


Cross-field зависимости и реактивность React Hook Form

React Hook Form оптимизирован для минимальных перерендеров, поэтому cross-field валидация требует понимания того, как обновляются зависимости.

При изменении одного поля:

  • пересчитывается схема через resolver
  • пересоздаётся объект ошибок
  • обновляются зависимые поля

Однако не все зависимости отслеживаются автоматически на уровне UI. Для корректной UX-реакции часто используется trigger.

useEffect(() => {
  trigger("confirmPassword");
}, [watch("password")]);

Это особенно важно, когда визуальная ошибка должна исчезать сразу после изменения связанного поля.


Типичные паттерны cross-field валидации

Подтверждение email

email: yup.string().email().required(),
confirmEmail: yup
  .string()
  .oneOf([yup.ref("email")], "Email не совпадает")

Условное обязательное поле

hasPhone: yup.boolean(),

phone: yup.string().when("hasPhone", {
  is: true,
  then: schema => schema.required(),
  otherwise: schema => schema.notRequired()
});

Взаимосвязанные флаги

acceptTerms: yup.boolean().oneOf([true]),
acceptPrivacy: yup.boolean().when("acceptTerms", {
  is: true,
  then: schema => schema.oneOf([true])
});

Ошибки проектирования cross-field схем

Одной из распространённых проблем является чрезмерное усложнение схемы, когда бизнес-логика полностью переносится в Yup.

Проблемные паттерны:

  • вложенные when на 3+ уровня
  • циклические зависимости полей
  • дублирование условий в нескольких полях
  • использование test вместо структурной логики

Циклические зависимости особенно критичны:

fieldA: yup.string().when("fieldB", ...),
fieldB: yup.string().when("fieldA", ...)

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


Оптимизация cross-field валидации

Для сложных форм важно минимизировать количество пересчётов схемы.

Практика:

  • использовать ref вместо test, где возможно
  • избегать лишних when
  • разделять схемы на логические блоки
  • использовать memoization схемы при необходимости

Пример стабильного определения схемы:

const schema = useMemo(() => yup.object({
  password: yup.string().required(),
  confirmPassword: yup.string().oneOf([yup.ref("password")])
}), []);

Поведение ошибок при cross-field изменениях

YupResolver возвращает ошибки в формате:

{
  type: "validation",
  message: "Пароли не совпадают",
  ref: "confirmPassword"
}

При изменении зависимого поля ошибка может:

  • сохраняться до повторной валидации
  • исчезать автоматически при совпадении значений
  • пересоздаваться при изменении родительского поля

Это поведение зависит от режима mode в React Hook Form (onChange, onBlur, onSubmit).


Вложенные структуры и cross-field зависимости

При работе с массивами и вложенными объектами cross-field логика усложняется:

items: yup.array().of(
  yup.object({
    price: yup.number(),
    discountPrice: yup
      .number()
      .max(yup.ref("price"), "Скидка не может превышать цену")
  })
);

Здесь каждый элемент массива имеет собственный контекст, и ref работает внутри конкретного объекта массива.


Стабильность и предсказуемость схемы

Cross-field валидация должна сохранять детерминированность. Основной принцип: схема должна описывать правила, а не управлять состоянием.

YupResolver обеспечивает выполнение этого принципа, поскольку:

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

Это делает cross-field логику декларативной, а не императивной.