Валидационные схемы в экосистеме Yup и связке с
YupResolver часто становятся центральным элементом
бизнес-логики форм. При росте приложения дублирование правил валидации
приводит к расхождению поведения между формами, усложняет сопровождение
и повышает риск несогласованности данных. Повторное использование
валидационной логики позволяет централизовать правила, строить
композиции схем и формировать единый источник истины для проверки
данных.
Ключевая идея заключается в том, что схема перестаёт быть локальным объектом конкретной формы и превращается в переиспользуемый модуль, который может расширяться, комбинироваться и адаптироваться под разные сценарии.
Наиболее простой уровень повторного использования — выделение базовых схем, описывающих сущности предметной области. Такие схемы содержат только универсальные правила, не привязанные к конкретной форме.
Пример базовой схемы пользователя:
import * as Yup from "yup";
export const userBaseSchema = Yup.object({
email: Yup.string()
.email("Некорректный email")
.required("Email обязателен"),
password: Yup.string()
.min(8, "Минимум 8 символов")
.required("Пароль обязателен"),
});
Данная схема может использоваться как основа для регистрации, авторизации или профиля, но в каждом случае расширяется дополнительными полями или правилами.
Повторное использование достигается за счёт неизменности базовой структуры и отсутствия контекста формы внутри неё.
Композиция позволяет создавать новые схемы на основе существующих, добавляя или переопределяя отдельные поля.
.concatYup предоставляет механизм объединения схем:
export const registrationSchema = userBaseSchema.concat(
Yup.object({
confirmPassword: Yup.string()
.oneOf([Yup.ref("password")], "Пароли не совпадают")
.required("Подтверждение обязательно"),
})
);
Такой подход сохраняет базовые правила и добавляет контекст регистрации.
Важно учитывать, что concat работает на уровне объектов
схем и может приводить к конфликтам при совпадении ключей, если не
контролировать порядок объединения.
Более гибкий подход — использование функций-фабрик, возвращающих схемы на основе входных параметров. Это позволяет адаптировать правила под условия интерфейса или бизнес-логики.
export const createPasswordSchema = ({ strong = false } = {}) => {
return Yup.string()
.required("Пароль обязателен")
.min(8, "Минимум 8 символов")
.test(
"strength",
"Пароль слишком слабый",
value => {
if (!strong) return true;
return /[A-Z]/.test(value) && /[0-9]/.test(value);
}
);
};
Использование:
const schema = Yup.object({
password: createPasswordSchema({ strong: true }),
});
Такой подход устраняет необходимость дублирования похожих правил с небольшими различиями.
Частичные схемы позволяют выделять логические блоки полей и комбинировать их в различных формах.
export const addressSchema = Yup.object({
country: Yup.string().required(),
city: Yup.string().required(),
street: Yup.string().required(),
});
export const profileSchema = Yup.object({
firstName: Yup.string().required(),
lastName: Yup.string().required(),
}).concat(addressSchema);
Такое разделение снижает связность и позволяет использовать
addressSchema в множестве сценариев: доставка, профиль,
оформление заказа.
.shape и расширение объектовПри работе с объектными схемами можно формировать расширения через
доступ к shape и создание новых объектов на его основе.
const baseShape = {
email: Yup.string().email().required(),
};
const extendedSchema = Yup.object({
...baseShape,
role: Yup.string().oneOf(["user", "admin"]).required(),
});
Этот подход ближе к функциональному расширению и удобен при динамической сборке схем.
Выделение валидаторов на уровень функций позволяет стандартизировать правила и переиспользовать их в любых схемах.
export const requiredString = (message = "Обязательное поле") =>
Yup.string().required(message);
export const emailField = () =>
Yup.string().email("Некорректный email").required("Email обязателен");
Использование:
const schema = Yup.object({
email: emailField(),
name: requiredString("Имя обязательно"),
});
Такой подход уменьшает когнитивную нагрузку и делает схемы декларативными.
Сложные формы часто требуют изменения валидации в зависимости от состояния данных. Вместо дублирования схем используется условная композиция.
.when как инструмент
адаптацииconst schema = Yup.object({
hasCompany: Yup.boolean(),
companyName: Yup.string().when("hasCompany", {
is: true,
then: schema => schema.required("Название компании обязательно"),
otherwise: schema => schema.notRequired(),
}),
});
Условная логика позволяет использовать одну и ту же структуру формы в разных сценариях без создания отдельных схем.
При работе с массивами и сложными структурами логика валидации часто повторяется на уровне элементов.
const productSchema = Yup.object({
title: Yup.string().required(),
price: Yup.number().positive().required(),
});
const orderSchema = Yup.object({
items: Yup.array().of(productSchema).min(1),
});
Здесь productSchema становится универсальной единицей,
используемой в различных контекстах: корзина, заказ, каталог.
В крупных приложениях схемы начинают отражать бизнес-правила, а не структуру форм. В этом случае важно отделять доменную валидацию от UI-уровня.
Пример разделения:
// domain/validation/userRules.js
export const emailRule = Yup.string().email().required();
export const passwordRule = Yup.string().min(8).required();
// form schemas
export const loginSchema = Yup.object({
email: emailRule,
password: passwordRule,
});
Такое разделение делает доменную логику независимой от библиотек форм и позволяет использовать её вне UI.
В контексте YupResolver повторное использование схем
напрямую влияет на поведение форм, так как резолвер работает с готовой
схемой без дополнительной логики.
import { yupResolver } from "@hookform/resolvers/yup";
const resolver = yupResolver(loginSchema);
При изменении базовых схем автоматически обновляется поведение всех форм, использующих эти схемы, что усиливает эффект централизованной валидации.
Для сложных систем применяется разделение на уровни:
Пример слоистой структуры:
// primitives
const requiredString = () => Yup.string().required();
// domain
const email = () => Yup.string().email().required();
// entity
const user = Yup.object({
email: email(),
name: requiredString(),
});
// scenario
const signup = user.concat(
Yup.object({
password: Yup.string().min(8).required(),
})
);
Такая структура снижает дублирование и упрощает масштабирование.
При изменении бизнес-логики важно не модифицировать каждую схему отдельно. Вместо этого изменения вносятся на уровне базовых правил или фабрик.
Например, изменение политики паролей:
export const passwordRule = () =>
Yup.string()
.min(12, "Минимум 12 символов")
.matches(/[A-Z]/, "Требуется заглавная буква")
.required();
Все формы автоматически наследуют новое поведение без дополнительного рефакторинга.
Иногда требуется динамически собирать массивы правил:
const rules = [
Yup.string().required(),
Yup.string().email(),
];
const schema = Yup.string().concat(rules[1]);
Хотя такой подход используется реже, он полезен при генерации схем на основе конфигураций.
При построении форм на основе конфигураций схемы становятся результатом вычислений:
const buildField = config => {
let schema = Yup.string();
if (config.required) {
schema = schema.required();
}
if (config.type === "email") {
schema = schema.email();
}
return schema;
};
const schema = Yup.object({
email: buildField({ type: "email", required: true }),
});
Такой подход позволяет строить формы без дублирования логики валидации.
Чрезмерная абстракция может привести к снижению читаемости схем. Слишком глубокая композиция или большое количество фабрик усложняет понимание фактических правил валидации на уровне конкретной формы.
Баланс достигается через разделение:
.whenПри активном переиспользовании важна строгая дисциплина:
Такая организация предотвращает расхождение логики между различными
частями приложения и обеспечивает предсказуемость поведения
YupResolver во всех формах.