При разработке крупных форм повторение одинаковых правил валидации быстро превращается в проблему поддержки. Одни и те же ограничения появляются в формах регистрации, редактирования профиля, оформления заказа, административных панелях и API-валидации. Повторяющийся код усложняет сопровождение и повышает вероятность ошибок.
Связка Yup и yupResolver позволяет строить
переиспользуемые схемы, разделять правила на независимые модули и
собирать сложную валидацию из небольших компонентов.
Наиболее простой вариант — вынесение схемы в отдельный файл.
import * as yup from 'yup';
export const userSchema = yup.object({
name: yup
.string()
.required('Имя обязательно')
.min(2, 'Минимум 2 символа'),
email: yup
.string()
.email('Некорректный email')
.required('Email обязателен'),
});
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { userSchema } from './schemas/user.schema';
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: yupResolver(userSchema),
});
Такой подход уже устраняет дублирование и позволяет использовать одну схему в нескольких компонентах.
Повторяются не только целые схемы, но и отдельные поля:
import * as yup from 'yup';
export const emailRule = yup
.string()
.email('Некорректный email')
.required('Email обязателен');
export const passwordRule = yup
.string()
.required('Пароль обязателен')
.min(8, 'Минимум 8 символов')
.matches(/[A-Z]/, 'Нужна заглавная буква')
.matches(/[0-9]/, 'Нужна цифра');
export const phoneRule = yup
.string()
.matches(/^\+?[0-9]{11,14}$/, 'Некорректный телефон');
import * as yup from 'yup';
import {
emailRule,
passwordRule,
phoneRule,
} from './common.rules';
export const registerSchema = yup.object({
email: emailRule,
password: passwordRule,
phone: phoneRule,
});
Yup поддерживает объединение схем через .shape() и
.concat().
.shape()import * as yup from 'yup';
const baseUserSchema = yup.object({
name: yup.string().required(),
email: yup.string().email().required(),
});
const extendedUserSchema = baseUserSchema.shape({
age: yup.number().required().positive(),
});
Схема extendedUserSchema содержит:
name;email;age..concat()Метод .concat() объединяет две схемы.
const addressSchema = yup.object({
city: yup.string().required(),
street: yup.string().required(),
});
const profileSchema = baseUserSchema.concat(addressSchema);
Это особенно полезно при модульной архитектуре.
Во многих случаях схема зависит от параметров:
Для этого удобно использовать функции-фабрики.
import * as yup from 'yup';
export const createPasswordSchema = (isStrongPassword) => {
return yup.object({
password: isStrongPassword
? yup
.string()
.required()
.min(12)
.matches(/[A-Z]/)
.matches(/[0-9]/)
: yup
.string()
.required()
.min(6),
});
};
const schema = createPasswordSchema(true);
useForm({
resolver: yupResolver(schema),
});
В крупных приложениях вложенные объекты появляются постоянно:
import * as yup from 'yup';
export const addressSchema = yup.object({
city: yup.string().required(),
street: yup.string().required(),
zipCode: yup.string().required(),
});
import * as yup from 'yup';
import { addressSchema } from './address.schema';
export const userSchema = yup.object({
name: yup.string().required(),
address: addressSchema,
});
import * as yup from 'yup';
export const productSchema = yup.object({
title: yup.string().required(),
price: yup.number().required().positive(),
});
import * as yup from 'yup';
import { productSchema } from './product.schema';
export const cartSchema = yup.object({
products: yup.array().of(productSchema),
});
Такой подход особенно полезен при работе с:
useFieldArray;Повторяющиеся модификации схем удобно оформлять как функции.
import * as yup from 'yup';
export const requiredString = (message = 'Поле обязательно') =>
yup.string().required(message);
const schema = yup.object({
firstName: requiredString(),
lastName: requiredString(),
});
export const createLimitedString = (max) =>
yup.string().max(max);
const schema = yup.object({
description: createLimitedString(300),
});
В административных интерфейсах формы создания и редактирования часто совпадают лишь частично.
const baseProductSchema = {
title: yup.string().required(),
price: yup.number().required(),
};
export const createProductSchema = yup.object({
...baseProductSchema,
image: yup
.mixed()
.required('Изображение обязательно'),
});
export const updateProductSchema = yup.object({
...baseProductSchema,
image: yup.mixed(),
});
Такой подход избавляет от копирования одинаковых полей.
Иногда требуется расширять существующие правила.
const usernameRule = yup
.string()
.required()
.min(3);
const adminUsernameRule = usernameRule.matches(
/^admin_/,
'Должен начинаться с admin_'
);
В административных системах поля могут приходить с сервера.
const fields = [
{
name: 'title',
type: 'string',
required: true,
},
{
name: 'price',
type: 'number',
required: true,
},
];
import * as yup from 'yup';
const generateSchema = (fields) => {
const shape = {};
fields.forEach((field) => {
let validator;
switch (field.type) {
case 'string':
validator = yup.string();
break;
case 'number':
validator = yup.number();
break;
default:
validator = yup.mixed();
}
if (field.required) {
validator = validator.required();
}
shape[field.name] = validator;
});
return yup.object(shape);
};
В больших приложениях схемы быстро разрастаются. Удобно организовывать их по доменам.
src/
├── schemas/
│ ├── common/
│ │ ├── rules.js
│ │ ├── messages.js
│ │ └── helpers.js
│ │
│ ├── user/
│ │ ├── user.schema.js
│ │ ├── profile.schema.js
│ │ └── address.schema.js
│ │
│ ├── product/
│ │ ├── product.schema.js
│ │ └── category.schema.js
│ │
│ └── order/
│ ├── order.schema.js
│ └── payment.schema.js
Текст ошибок тоже желательно централизовать.
export const validationMessages = {
required: 'Поле обязательно',
invalidEmail: 'Некорректный email',
minPassword: 'Минимум 8 символов',
};
import * as yup from 'yup';
import { validationMessages } from './messages';
const schema = yup.object({
email: yup
.string()
.email(validationMessages.invalidEmail)
.required(validationMessages.required),
});
Yup поддерживает глобальную локализацию.
import * as yup from 'yup';
yup.setLocale({
mixed: {
required: 'Поле обязательно',
},
string: {
email: 'Некорректный email',
min: 'Слишком короткое значение',
},
});
После этого сообщения автоматически применяются ко всем схемам.
const paymentSchema = yup.object({
paymentMethod: yup.string().required(),
cardNumber: yup.string().when('paymentMethod', {
is: 'card',
then: (schema) =>
schema.required('Введите номер карты'),
otherwise: (schema) =>
schema.notRequired(),
}),
});
export const cardRequiredRule = yup.string().when(
'paymentMethod',
{
is: 'card',
then: (schema) =>
schema.required(),
otherwise: (schema) =>
schema.notRequired(),
}
);
const schema = yup.object({
paymentMethod: yup.string().required(),
cardNumber: cardRequiredRule,
});
const slugRule = yup
.string()
.test(
'slug-format',
'Некорректный slug',
(value) => {
if (!value) return false;
return /^[a-z0-9-]+$/.test(value);
}
);
const articleSchema = yup.object({
slug: slugRule,
});
Для действительно глобального переиспользования Yup позволяет добавлять собственные методы.
import * as yup from 'yup';
yup.addMethod(
yup.string,
'phone',
function phone(message) {
return this.matches(
/^\+?[0-9]{11,14}$/,
message || 'Некорректный телефон'
);
}
);
const schema = yup.object({
phone: yup
.string()
.phone(),
});
При использовании TypeScript схемы становятся источником типов.
import * as yup from 'yup';
const userSchema = yup.object({
name: yup.string().required(),
age: yup.number().required(),
});
type User = yup.InferType<typeof userSchema>;
Тип User автоматически синхронизируется со схемой.
При большом количестве схем легко получить циклические импорты.
user.schema.js
└── import profile.schema.js
profile.schema.js
└── import user.schema.js
common;Создание схем внутри компонента приводит к лишним вычислениям.
const Component = () => {
const schema = yup.object({
name: yup.string().required(),
});
useForm({
resolver: yupResolver(schema),
});
};
Схема создаётся при каждом рендере.
const schema = yup.object({
name: yup.string().required(),
});
const Component = () => {
useForm({
resolver: yupResolver(schema),
});
};
Если схема зависит от параметров:
const schema = useMemo(() => {
return createUserSchema(role);
}, [role]);
Это предотвращает повторную генерацию схемы.
Yup можно использовать не только в React Hook Form.
useForm({
resolver: yupResolver(userSchema),
});
await userSchema.validate(data);
Преимущества:
В крупных проектах удобно использовать отдельные builders.
export const buildUserSchema = ({
requirePhone,
requireAddress,
}) => {
return yup.object({
name: yup.string().required(),
phone: requirePhone
? yup.string().required()
: yup.string(),
address: requireAddress
? addressSchema.required()
: addressSchema,
});
};
Каждое правило — независимый модуль:
emailRule
passwordRule
phoneRule
Преимущества:
Схема представляет полноценную бизнес-сущность:
userSchema
productSchema
orderSchema
Преимущества:
Наиболее распространённая архитектура:
Именно такой подход обеспечивает масштабируемость крупных приложений на React Hook Form и YupResolver.