Зависимые поля

Зависимые поля в схемах валидации становятся критическим механизмом, когда значение одного поля определяет правила проверки другого. В связке Yup и YupResolver такая логика реализуется на уровне схемы, а не в компоненте формы, что позволяет сохранять декларативность и предсказуемость поведения валидации.

Основная идея заключается в том, что схема описывает не только статические ограничения (например, обязательность или формат), но и динамические зависимости между значениями. В контексте Yup это достигается через условные конструкции, такие как when, ссылки через ref, а также пользовательские валидаторы test.

При использовании YupResolver схема получает актуальные значения формы при каждом изменении состояния. Resolver выступает промежуточным слоем между библиотекой управления формой (например, React Hook Form) и Yup-схемой. На вход передаются:

  • текущие значения формы;
  • контекст (если используется);
  • схема Yup.

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

Ключевой момент: зависимость полей не требует ручного пересчёта состояния — она описывается внутри схемы и автоматически пересчитывается при изменении значений.

Условная валидация через when

Наиболее распространённый механизм зависимых полей — метод when. Он позволяет изменять правила валидации одного поля в зависимости от значения другого.

Простейший пример: поле passportNumber становится обязательным только если hasPassport равно true.

import * as Yup from "yup";

const schema = Yup.object({
  hasPassport: Yup.boolean(),
  passportNumber: Yup.string().when("hasPassport", {
    is: true,
    then: (schema) => schema.required("Укажите номер паспорта"),
    otherwise: (schema) => schema.notRequired()
  })
});

Здесь происходит декларативное описание зависимости: поле passportNumber не хранит собственную логику, оно полностью подчиняется состоянию hasPassport.

Внутри YupResolver это обрабатывается на этапе валидации: при каждом изменении hasPassport пересчитывается валидность passportNumber.

Зависимость от нескольких полей

Более сложные сценарии включают зависимость от нескольких источников данных. Yup поддерживает передачу массива зависимостей в when.

Пример: поле deliveryAddress обязательно, если выбран способ доставки courier и заказ не является самовывозом.

const schema = Yup.object({
  deliveryMethod: Yup.string(),
  isPickup: Yup.boolean(),
  deliveryAddress: Yup.string().when(
    ["deliveryMethod", "isPickup"],
    ([deliveryMethod, isPickup], schema) => {
      return deliveryMethod === "courier" && !isPickup
        ? schema.required("Укажите адрес доставки")
        : schema.notRequired();
    }
  )
});

Такая конструкция позволяет формировать сложные правила без разрастания логики в компоненте формы.

Использование ref для связи полей

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

Пример: подтверждение пароля должно совпадать с основным паролем.

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

Здесь Yup.ref("password") создаёт динамическую ссылку на текущее значение поля password. При любом изменении пароля пересчитывается валидность подтверждения.

В контексте YupResolver это особенно важно: пересчёт происходит автоматически без необходимости вручную триггерить валидацию зависимых полей.

Контекстная зависимость через context

Иногда зависимость полей выходит за рамки значений формы. В таких случаях используется context, который передаётся в resolver и доступен внутри схемы.

Пример: валидация зависит от роли пользователя.

const schema = Yup.object({
  discountCode: Yup.string().when("$role", (role, schema) => {
    return role === "admin"
      ? schema.notRequired()
      : schema.required("Введите промокод");
  })
});

При использовании YupResolver контекст передаётся следующим образом:

useForm({
  resolver: yupResolver(schema, { context: { role: "user" } })
});

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

Каскадные зависимости

В сложных формах часто возникает цепочка зависимостей, где одно поле влияет на второе, а второе — на третье. Например:

  • выбор типа пользователя влияет на доступные поля;
  • доступные поля влияют на обязательность ввода;
  • обязательность ввода влияет на формат.
const schema = Yup.object({
  accountType: Yup.string(),
  companyName: Yup.string().when("accountType", {
    is: "business",
    then: (s) => s.required(),
    otherwise: (s) => s.notRequired()
  }),
  vatNumber: Yup.string().when(["accountType", "companyName"], {
    is: (accountType, companyName) =>
      accountType === "business" && !!companyName,
    then: (s) => s.required("Укажите VAT номер"),
    otherwise: (s) => s.notRequired()
  })
});

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

Пользовательские проверки через test

Когда логика зависимости выходит за пределы стандартных методов, используется test. Он предоставляет полный доступ к значению поля и всей родительской структуре.

Пример: поле должно быть больше другого поля.

const schema = Yup.object({
  minValue: Yup.number(),
  maxValue: Yup.number().test(
    "is-greater",
    "maxValue должен быть больше minValue",
    function (value) {
      const { minValue } = this.parent;
      return value > minValue;
    }
  )
});

Здесь this.parent предоставляет доступ ко всем полям объекта. Это делает возможной реализацию произвольных зависимостей, которые невозможно выразить через when.

Пересчёт зависимостей в YupResolver

При каждом изменении значения формы YupResolver вызывает повторную валидацию схемы. Важно, что Yup не отслеживает изменения реактивно — пересчёт полностью управляется внешним слоем.

Это означает:

  • любое изменение поля потенциально может затронуть другие поля;
  • схема всегда валидируется целиком или частично, в зависимости от реализации resolver;
  • зависимые поля обновляют своё состояние автоматически через результат валидации.

Такой подход упрощает архитектуру, но требует аккуратной работы с производительностью в больших формах.

Порядок вычисления и потенциальные коллизии

При сложных зависимостях возможны ситуации, когда несколько правил конфликтуют друг с другом. Например, одно правило делает поле обязательным, другое — запрещает его заполнение.

field: Yup.string()
  .when("flag", {
    is: true,
    then: (s) => s.required(),
    otherwise: (s) => s.notRequired()
  })
  .when("disabled", {
    is: true,
    then: (s) => s.strip()
  })

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

Интеграция зависимых полей с React Hook Form

В связке с React Hook Form зависимые поля получают дополнительное поведение за счёт подписки на изменения.

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

const accountType = watch("accountType");

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

Схема остаётся единственным источником истины, а UI лишь отображает её состояние.

Оптимизация сложных зависимостей

При большом количестве зависимых полей возникает риск избыточных пересчётов. Чтобы снизить нагрузку, используются следующие подходы:

  • минимизация глубины вложенных when;
  • разделение схемы на логические блоки через object().shape;
  • использование lazy для отложенного построения схем;
  • вынесение повторяющейся логики в функции-схемы.
const requiredIfBusiness = (schema) =>
  schema.when("accountType", {
    is: "business",
    then: (s) => s.required(),
    otherwise: (s) => s.notRequired()
  });

Такой подход снижает дублирование и улучшает читаемость при сохранении декларативной структуры.

Поведение при асинхронных зависимостях

Yup поддерживает асинхронную валидацию через test, что позволяет реализовывать зависимости от внешних данных, например проверку уникальности.

username: Yup.string().test(
  "unique",
  "Имя уже занято",
  async (value) => {
    const result = await api.checkUsername(value);
    return result.available;
  }
)

В сочетании с YupResolver такие проверки становятся частью общей схемы, однако увеличивают время валидации и могут влиять на UX при частых изменениях полей.

Поведение ошибок при изменении зависимых полей

При изменении значения поля, от которого зависят другие поля, Yup пересчитывает ошибки для всей ветки схемы. Это означает, что:

  • ошибка может появиться в другом поле, даже если оно не изменялось;
  • ранее валидное поле может стать невалидным из-за изменения зависимости;
  • состояние формы всегда согласовано с актуальной схемой.

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