Проблемы с refs и зависимостями

Механизм ссылок (ref) и его поведение

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

Типичный пример:

import * as Yup from "yup";

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

Здесь confirmPassword зависит от значения password. Однако именно такие зависимости часто становятся источником скрытых проблем при усложнении схем.


Контекстная природа ссылок и момент разрешения значений

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

  • порядок объявления полей не всегда гарантирует порядок вычисления;
  • значение может быть недоступно в момент построения схемы;
  • поведение зависит от контекста выполнения validate.

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

const schema = Yup.object({
  user: Yup.object({
    password: Yup.string().required(),
    confirm: Yup.string().oneOf([Yup.ref("password")]),
  }),
});

Здесь Yup.ref("password") не указывает на user.password, а ищет поле на уровне текущего объекта. Это частый источник ошибок.


Локальные и глобальные области видимости ref

ref всегда интерпретируется относительно текущего уровня схемы.

Рассмотрим структуру:

{
  user: {
    password: "123",
    confirm: "123"
  }
}

Корректная ссылка:

Yup.ref("password")

Неверная попытка:

Yup.ref("user.password") // не работает так, как ожидается

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


Проблемы с циклическими зависимостями

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

Пример:

const schema = Yup.object({
  a: Yup.number().when("b", (b, schema) =>
    b > 10 ? schema.min(5) : schema.max(5)
  ),
  b: Yup.number().when("a", (a, schema) =>
    a > 10 ? schema.min(5) : schema.max(5)
  ),
});

Здесь возникает логический цикл:

  • a зависит от b
  • b зависит от a

В результате:

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

Yup не строит граф зависимостей заранее, поэтому такие конструкции приводят к непредсказуемому поведению.


when и динамические зависимости

Метод when является основным инструментом для построения зависимостей:

Yup.string().when("isRequired", (isRequired, schema) => {
  return isRequired ? schema.required() : schema;
});

Проблемы возникают, когда:

  1. зависимость отсутствует в текущем объекте;
  2. значение undefined трактуется неоднозначно;
  3. тип значения не совпадает с ожидаемым;
  4. используется несколько зависимостей одновременно.

Пример множественных зависимостей:

Yup.string().when(["isAdmin", "country"], (isAdmin, country, schema) => {
  if (isAdmin && country === "KZ") {
    return schema.required();
  }
  return schema;
});

Здесь сложность возрастает экспоненциально при добавлении новых условий, а диагностика ошибок становится затруднительной.


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

В связке с формами (например, React-экосистема) часто возникает проблема устаревших значений.

Схема может быть валидирована на основе старого состояния, если:

  • изменение поля не триггерит повторную валидацию;
  • кешируется результат validate;
  • используется оптимизация resolver’ов.

В результате:

  • ref возвращает актуальное значение только при повторной валидации;
  • UI может показывать несогласованное состояние ошибок.

Вложенные объекты и потеря контекста ref

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

const schema = Yup.object({
  settings: Yup.object({
    min: Yup.number(),
    max: Yup.number().min(Yup.ref("min")),
  }),
});

Ожидаемое поведение — ссылка на settings.min, но фактически ref("min") ищет min внутри текущего уровня settings.

Корректный способ:

Yup.ref("settings.min")

Однако даже этот вариант может работать нестабильно в зависимости от контекста вызова.


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

В массивах проблема усложняется ещё сильнее:

const schema = Yup.object({
  items: Yup.array().of(
    Yup.object({
      min: Yup.number(),
      value: Yup.number().min(Yup.ref("min")),
    })
  ),
});

Здесь ref("min") должен ссылаться на поле внутри текущего элемента массива. Это работает только при корректной локализации контекста элемента.

Проблемы возникают при:

  • использовании transform;
  • кастомных test;
  • динамическом изменении структуры массива;
  • удалении или вставке элементов.

Потеря типизации и зависимые поля в TypeScript

При использовании TypeScript в связке с Yup возникают дополнительные сложности:

  • ref возвращает any;
  • отсутствует строгая проверка существования поля;
  • ошибки проявляются только в runtime.

Пример:

Yup.string().oneOf([Yup.ref("password")])

Компилятор не гарантирует, что password существует в схеме, что увеличивает риск скрытых ошибок.


Проблемы с lazy и динамическими схемами

lazy позволяет строить схему на основе значения:

Yup.lazy((value) => {
  if (typeof value === "string") {
    return Yup.string().required();
  }
  return Yup.number();
});

При сочетании с ref появляются следующие сложности:

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

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

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

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

Частые ошибки при проектировании зависимостей

На практике наиболее распространены следующие проблемы:

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

Поведенческие особенности при ошибках разрешения

Когда ref не может быть разрешён:

  • возвращается undefined;
  • часть правил игнорируется;
  • валидатор продолжает выполнение без явного исключения.

Это приводит к ситуациям, когда:

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

Итеративное усложнение зависимостей

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

Это приводит к:

  • скрытым циклам;
  • трудностям рефакторинга;
  • высокой стоимости изменений;
  • непредсказуемому поведению при расширении схемы.

Итоговая картина проблемной зоны

Работа с ref и зависимостями в Yup характеризуется сочетанием нескольких факторов:

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

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