При использовании схемной валидации в связке с Yup и формами на базе React Hook Form ключевым фактором становится предсказуемая структура проекта. YupResolver выступает связующим адаптером между схемой и формой, поэтому любые ошибки в именовании или размещении файлов быстро приводят к дублированию логики и снижению читаемости.
Основная цель структуры — отделить:
Такое разделение позволяет воспринимать валидацию как самостоятельный слой приложения, а не как часть компонента.
Схема валидации должна отражать предметную область, а не техническую реализацию. Использование абстрактных имен приводит к потере контекста при масштабировании.
Хорошая практика:
userSchemaauthLoginSchemaprofileUpdateSchemaorderCreateSchemaПлохая практика:
schema1validationSchemaformSchemaNewВажный момент заключается в том, что имя схемы должно совпадать с названием бизнес-сущности, а не формы. Например, если форма используется в двух разных UI-компонентах, схема остаётся одна.
При использовании @hookform/resolvers с YupResolver схемы целесообразно выносить в отдельный слой.
Рекомендуемая структура:
src/
validation/
schemas/
auth/
login.schema.ts
register.schema.ts
user/
profile.schema.ts
resolvers/
auth.resolvers.ts
Принцип построения:
Файл схемы не должен содержать UI-логики или зависимостей от компонентов.
Поле в схеме должно соответствовать данным, которые приходят с формы или API. Несоответствие имен приводит к дополнительным маппингам и усложняет поддержку.
Пример корректного описания:
import * as yup from "yup";
export const loginSchema = yup.object({
email: yup.string().email().required(),
password: yup.string().min(8).required(),
});
Здесь:
email и password совпадают с именами
input-полей,field1 или
inputA.Если данные API отличаются от формы, преобразование должно происходить отдельно, а не внутри схемы.
YupResolver используется как адаптер между схемой и формой:
import { yupResolver } from "@hookform/resolvers/yup";
import { loginSchema } from "../schemas/auth/login.schema";
export const loginResolver = yupResolver(loginSchema);
Правила именования:
loginResolver — для конкретной формы,authLoginResolver — если требуется уточнение
домена,resolver.Если в проекте несколько форм с одной схемой, резолвер не должен дублироваться, а импортироваться повторно.
Структурно связь должна быть однонаправленной:
schema → resolver → form
Форма не должна знать о деталях схемы, кроме передачи в
useForm.
Пример:
import { useForm } from "react-hook-form";
import { loginResolver } from "../validation/resolvers/auth.resolvers";
export function LoginForm() {
const form = useForm({
resolver: loginResolver,
});
return null;
}
Такой подход исключает смешивание уровней ответственности.
Ошибки, возвращаемые Yup через YupResolver, автоматически маппятся в
структуру formState.errors.
Важно поддерживать единый стиль обращения:
errors.emailerrors.passwordНе рекомендуется:
Если требуется трансформация сообщений, она должна быть частью схемы:
email: yup
.string()
.email("Некорректный формат email")
.required("Email обязателен");
При увеличении количества полей схема должна сохранять модульность.
Подход — декомпозиция:
const passwordRules = yup.string().min(8).required();
export const registerSchema = yup.object({
email: yup.string().email().required(),
password: passwordRules,
confirmPassword: yup
.string()
.oneOf([yup.ref("password")])
});
Общие правила:
При работе с объектами важно сохранять иерархию данных:
const addressSchema = yup.object({
city: yup.string().required(),
street: yup.string().required(),
});
export const userSchema = yup.object({
name: yup.string().required(),
address: addressSchema,
});
Имена:
addressSchema, а не schemaAddress,Типичная ошибка — повторение схем для разных форм с минимальными отличиями. Вместо этого используется композиция:
const baseUserSchema = {
email: yup.string().email().required(),
};
export const createUserSchema = yup.object({
...baseUserSchema,
password: yup.string().min(8).required(),
});
export const updateUserSchema = yup.object({
...baseUserSchema,
});
Такое разделение снижает риск рассинхронизации требований.
В крупных кодовых базах одного имени недостаточно. Тогда применяется доменная приставка:
authLoginSchemabillingInvoiceSchemaprofileSettingsSchemaПринцип:
Это снижает вероятность конфликтов имён при масштабировании.
Стабильная единица модуля выглядит следующим образом:
auth/
login/
login.schema.ts
login.resolver.ts
login.types.ts
Каждый элемент отвечает за одну задачу:
Такой подход обеспечивает предсказуемость и упрощает навигацию по проекту без необходимости анализа зависимостей в рантайме.