При использовании валидации на основе Yup в связке с резолверами для react-hook-form ключевым фактором масштабируемости становится организация схем. Отсутствие структуры приводит к дублированию правил, усложнению поддержки и росту количества логических ошибок при изменении форм.
Базовый принцип заключается в том, что схема не должна быть привязана к конкретному компоненту интерфейса. Она должна отражать доменную модель данных: пользователь, заказ, профиль, фильтры поиска, платежные данные. Такой подход позволяет использовать одну и ту же схему в разных контекстах: создание, редактирование, серверная валидация, тестирование.
Практика разбиения схем по предметным областям проекта обеспечивает предсказуемость и упрощает навигацию:
src/
validation/
user/
user.schema.ts
user.update.schema.ts
user.create.schema.ts
auth/
login.schema.ts
register.schema.ts
order/
order.schema.ts
order.item.schema.ts
shared/
address.schema.ts
email.schema.ts
phone.schema.ts
Каждая директория соответствует домену. Общие элементы выносятся в
shared, чтобы избежать дублирования базовых правил.
В Yup ключевой механизм повторного использования — композиция через
object().shape() и переиспользуемые фрагменты схем.
import * as yup from "yup";
export const emailSchema = yup
.string()
.email("Некорректный email")
.required("Email обязателен");
export const passwordSchema = yup
.string()
.min(8, "Минимум 8 символов")
.required("Пароль обязателен");
Такие примитивы используются в разных схемах без копирования логики.
import { emailSchema, passwordSchema } from "../shared";
export const loginSchema = yup.object({
email: emailSchema,
password: passwordSchema,
});
Композиционный подход снижает связанность и делает изменения локальными.
Даже в пределах одной сущности схемы часто различаются:
Пример разделения:
export const userCreateSchema = yup.object({
email: emailSchema,
password: passwordSchema,
name: yup.string().required(),
});
export const userUpdateSchema = yup.object({
email: emailSchema.optional(),
name: yup.string().optional(),
});
Такой подход предотвращает перегрузку одной универсальной схемы условной логикой.
В связке с react-hook-form схемы подключаются через резолвер, который адаптирует Yup-валидацию под систему управления формами:
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { loginSchema } from "./validation/auth/login.schema";
const form = useForm({
resolver: yupResolver(loginSchema),
});
Организация схем напрямую влияет на читаемость и поддерживаемость форм. При разрастании проекта становится критично, чтобы каждая форма ссылалась на компактную, изолированную схему.
Общие ограничения (например, формат email, требования к паролю, лимиты строк) выносятся в отдельный слой:
export const constraints = {
emailMax: 254,
nameMax: 50,
passwordMin: 8,
};
Использование констант в схемах:
export const emailSchema = yup
.string()
.email()
.max(constraints.emailMax)
.required();
Централизация правил снижает риск несогласованности между различными формами.
Механизм расширения позволяет строить новые схемы на основе существующих:
export const baseUserSchema = yup.object({
email: emailSchema,
name: yup.string().required(),
});
export const adminUserSchema = baseUserSchema.shape({
role: yup.string().required(),
permissions: yup.array().of(yup.string()),
});
Подобная структура особенно полезна в административных интерфейсах, где расширяются базовые модели данных.
Условные проверки внутри схем быстро усложняют поддержку. Более устойчивый подход — выделение отдельных схем под разные состояния:
export const paymentCardSchema = yup.object({
type: yup.mixed().oneOf(["card"]),
cardNumber: yup.string().required(),
});
export const paymentCryptoSchema = yup.object({
type: yup.mixed().oneOf(["crypto"]),
wallet: yup.string().required(),
});
Выбор схемы происходит на уровне формы, а не внутри валидационного дерева.
При использовании TypeScript схема становится источником типов:
import { InferType } from "yup";
export type LoginForm = InferType<typeof loginSchema>;
Размещение типов рядом со схемами или в отдельных файлах зависит от масштаба:
types/ или
contracts/Важно избегать рассинхронизации между схемой и типом.
Для повторяющихся структур используются фабрики:
export const createRequiredString = (max = 255) =>
yup.string().max(max).required();
export const createOptionalString = (max = 255) =>
yup.string().max(max).notRequired();
Применение:
export const profileSchema = yup.object({
bio: createOptionalString(500),
displayName: createRequiredString(50),
});
Такой подход стандартизирует правила и снижает количество ручного кода.
Схемы не должны зависеть от компонентов, маршрутов или состояния интерфейса. Нарушение этого принципа приводит к:
Схема должна оставаться чистой функцией описания данных.
При развитии продукта появляются несовместимые изменения. Организация версий позволяет избежать поломок:
validation/
user/
v1/
user.schema.ts
v2/
user.schema.ts
Каждая версия соответствует состоянию API или бизнес-логики на определённом этапе.
В крупных системах удобно собирать схемы через индексные файлы:
validation/
user/
index.ts
user.schema.ts
user.create.schema.ts
export * from "./user.schema";
export * from "./user.create.schema";
Это упрощает импортирование и уменьшает связанность модулей с внутренней структурой файлов.
Формы должны получать схему как зависимость, а не создавать её внутри:
function useLoginForm(schema) {
return useForm({
resolver: yupResolver(schema),
});
}
Такой подход позволяет подменять схемы для тестирования и различных окружений.
Одна и та же схема может использоваться частично, но чаще возникает необходимость различий:
Организация:
validation/
shared/
client/
server/
Это снижает риск расхождения правил между слоями системы.