В экосистеме валидации данных с использованием Yup одной из скрытых, но критичных проблем становится повторное создание схем при каждом рендере или вызове функции. Несмотря на то что сами схемы выглядят как обычные JavaScript-объекты, их внутренняя структура содержит цепочки валидаторов, трансформеров и ссылок, которые требуют затрат на инициализацию.
Каждое пересоздание схемы приводит к:
Особенно это заметно в приложениях, использующих React, где функции компонентов вызываются часто, а любая незафиксированная структура пересоздаётся автоматически.
Наиболее распространённая ошибка — объявление схемы непосредственно в теле функции компонента или обработчика:
function createValidation() {
return Yup.object({
email: Yup.string().email().required(),
password: Yup.string().min(8).required()
});
}
Или в React-компоненте:
function Form() {
const schema = Yup.object({
email: Yup.string().email().required(),
password: Yup.string().min(8).required()
});
// использование schema
}
Проблема заключается в том, что при каждом вызове Form
создаётся новый экземпляр схемы, даже если структура полностью
идентична.
Наиболее прямолинейное решение — вынести схему на уровень модуля:
const schema = Yup.object({
email: Yup.string().email().required(),
password: Yup.string().min(8).required()
});
Такая форма гарантирует:
Особенно важно это для библиотек вроде Formik или React Hook Form, которые сравнивают схемы по ссылке.
Часто требуется динамическая схема, зависящая от параметров (например, роли пользователя). В таких случаях возникает соблазн пересоздавать всю схему:
const createSchema = (isAdmin) =>
Yup.object({
name: Yup.string().required(),
...(isAdmin ? { role: Yup.string().required() } : {})
});
Проблема в том, что при каждом вызове создаётся новый объект.
Оптимальный подход — разделение базовой и расширяющей частей:
const baseSchemaShape = {
name: Yup.string().required()
};
const adminExtensionShape = {
role: Yup.string().required()
};
const baseSchema = Yup.object(baseSchemaShape);
const createSchema = (isAdmin) =>
isAdmin
? baseSchema.concat(Yup.object(adminExtensionShape))
: baseSchema;
Здесь базовая схема сохраняется неизменной, а расширение добавляется декларативно.
.concat() вместо пересборкиМетод concat в Yup позволяет объединять схемы без
разрушения исходной структуры:
const schemaA = Yup.object({
email: Yup.string().email().required()
});
const schemaB = Yup.object({
age: Yup.number().min(18)
});
const merged = schemaA.concat(schemaB);
Ключевое преимущество — schemaA и schemaB
остаются неизменными, что позволяет безопасно переиспользовать их в
разных частях приложения.
Даже при выносе схемы возможны сценарии, когда она зависит от props.
В этом случае часто применяется useMemo, но с ошибками:
const schema = useMemo(() => {
return Yup.object({
email: Yup.string().required(),
role: isAdmin ? Yup.string().required() : Yup.string()
});
}, [isAdmin]);
Хотя useMemo снижает частоту пересоздания, он не
устраняет сам факт генерации новой схемы при изменении зависимости.
Более стабильный подход — комбинирование предсобранных частей:
const baseSchema = Yup.object({
email: Yup.string().required()
});
const adminSchema = Yup.object({
role: Yup.string().required()
});
const schema = useMemo(() => {
return isAdmin ? baseSchema.concat(adminSchema) : baseSchema;
}, [isAdmin]);
Таким образом пересоздаётся только композиция, а не базовые валидаторы.
В архитектурах с большим числом форм схемы часто оформляются как отдельные модули-синглтоны:
export const loginSchema = Yup.object({
email: Yup.string().email().required(),
password: Yup.string().required()
});
Такой подход обеспечивает:
Фабричный подход оправдан только тогда, когда параметризация неизбежна, но важно избегать полного пересоздания структуры:
const createBaseSchema = () =>
Yup.object({
email: Yup.string().email().required()
});
const getSchema = (() => {
const cache = new Map();
return (key) => {
if (cache.has(key)) return cache.get(key);
const schema = createBaseSchema().concat(
key === "admin"
? Yup.object({ role: Yup.string().required() })
: Yup.object({})
);
cache.set(key, schema);
return schema;
};
})();
Кэширование снижает количество инициализаций и стабилизирует ссылочную модель.
Yup активно использует object(shape) как основу схемы.
Повторное создание shape приводит к каскадному пересозданию всей
структуры.
Рациональный подход — вынос shape:
const userShape = {
email: Yup.string().email().required(),
password: Yup.string().min(8).required()
};
const schema = Yup.object(userShape);
Если требуется расширение:
const adminShape = {
...userShape,
role: Yup.string().required()
};
const adminSchema = Yup.object(adminShape);
Важно, что сами валидаторы внутри shape также являются объектами, и их повторное создание нежелательно.
В сложных структурах с условной логикой применяется
Yup.lazy, позволяющий отложить создание схемы:
const schema = Yup.lazy((value) => {
if (value?.type === "admin") {
return Yup.object({
role: Yup.string().required()
});
}
return Yup.object({
email: Yup.string().required()
});
});
Хотя lazy не устраняет пересоздание полностью, он
позволяет ограничить область создания схем только реальными входными
данными, а не всем жизненным циклом приложения.
Каждый экземпляр схемы Yup содержит собственную цепочку:
transformtestwhenrequired и др.При пересоздании эти цепочки заново компилируются, что особенно дорого при глубокой вложенности объектов.
Пример проблемной структуры:
Yup.object({
user: Yup.object({
profile: Yup.object({
age: Yup.number().required()
})
})
});
Повторная генерация такой схемы увеличивает стоимость валидации пропорционально глубине дерева.
Многие библиотеки форм и кэширования используют простое сравнение:
prevSchema === nextSchemaПри пересоздании схемы даже с идентичной структурой это условие нарушается, что приводит к:
Стабильность ссылок становится важнее самой структуры схемы.
Оптимальная архитектура предполагает разделение:
const schema = Yup.object({
email: Yup.string().required()
});
const validate = async (data) => {
return schema.validate(data);
};
Такой подход устраняет необходимость пересоздания схемы даже при множественных вызовах валидации.