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 выполняет трансляцию схемы Yup в формат
ошибок React Hook Form. При этом он:
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,
который позволяет менять правила валидации динамически.
confirmPassword: yup
.string()
.oneOf([yup.ref("password")], "Пароли должны совпадать")
Используется при строгом равенстве значений.
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;
});
Одним из наиболее частых кейсов является проверка диапазона дат или чисел.
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 работает только в пределах одного уровня
объекта. При вложенных структурах необходимо учитывать путь поля.
const schema = yup.object({
user: yup.object({
password: yup.string(),
confirmPassword: yup.string().test(
"match",
"Ошибка",
function (value) {
return value === this.parent.password;
}
)
})
});
При глубокой вложенности важно помнить, что parent
относится к ближайшему объекту, а не к корню схемы.
React Hook Form оптимизирован для минимальных перерендеров, поэтому cross-field валидация требует понимания того, как обновляются зависимости.
При изменении одного поля:
Однако не все зависимости отслеживаются автоматически на уровне UI.
Для корректной UX-реакции часто используется trigger.
useEffect(() => {
trigger("confirmPassword");
}, [watch("password")]);
Это особенно важно, когда визуальная ошибка должна исчезать сразу после изменения связанного поля.
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])
});
Одной из распространённых проблем является чрезмерное усложнение схемы, когда бизнес-логика полностью переносится в Yup.
Проблемные паттерны:
when на 3+ уровняtest вместо структурной логикиЦиклические зависимости особенно критичны:
fieldA: yup.string().when("fieldB", ...),
fieldB: yup.string().when("fieldA", ...)
Такая схема приводит к непредсказуемому порядку вычислений.
Для сложных форм важно минимизировать количество пересчётов схемы.
Практика:
ref вместо test, где
возможноwhenПример стабильного определения схемы:
const schema = useMemo(() => yup.object({
password: yup.string().required(),
confirmPassword: yup.string().oneOf([yup.ref("password")])
}), []);
YupResolver возвращает ошибки в формате:
{
type: "validation",
message: "Пароли не совпадают",
ref: "confirmPassword"
}
При изменении зависимого поля ошибка может:
Это поведение зависит от режима mode в React Hook Form
(onChange, onBlur, onSubmit).
При работе с массивами и вложенными объектами cross-field логика усложняется:
items: yup.array().of(
yup.object({
price: yup.number(),
discountPrice: yup
.number()
.max(yup.ref("price"), "Скидка не может превышать цену")
})
);
Здесь каждый элемент массива имеет собственный контекст, и
ref работает внутри конкретного объекта массива.
Cross-field валидация должна сохранять детерминированность. Основной принцип: схема должна описывать правила, а не управлять состоянием.
YupResolver обеспечивает выполнение этого принципа, поскольку:
Это делает cross-field логику декларативной, а не императивной.