Избежание пересоздания схем

В экосистеме валидации данных с использованием Yup одной из скрытых, но критичных проблем становится повторное создание схем при каждом рендере или вызове функции. Несмотря на то что сами схемы выглядят как обычные JavaScript-объекты, их внутренняя структура содержит цепочки валидаторов, трансформеров и ссылок, которые требуют затрат на инициализацию.

Каждое пересоздание схемы приводит к:

  • повторной сборке цепочки валидаторов;
  • лишним аллокациям памяти;
  • увеличению времени выполнения валидации;
  • потере стабильности ссылочной идентичности объекта схемы;
  • деградации производительности в реактивных UI-сценариях.

Особенно это заметно в приложениях, использующих 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 остаются неизменными, что позволяет безопасно переиспользовать их в разных частях приложения.


Проблема пересоздания в React и useMemo

Даже при выносе схемы возможны сценарии, когда она зависит от 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()
});

Такой подход обеспечивает:

  • единый источник истины;
  • отсутствие дублирования;
  • предсказуемую работу кешей;
  • снижение нагрузки на GC.

Использование фабрик схем без разрушения кеширования

Фабричный подход оправдан только тогда, когда параметризация неизбежна, но важно избегать полного пересоздания структуры:

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;
  };
})();

Кэширование снижает количество инициализаций и стабилизирует ссылочную модель.


Переиспользование shape-объектов

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 также являются объектами, и их повторное создание нежелательно.


Lazy-схемы как способ отсроченной инициализации

В сложных структурах с условной логикой применяется 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 содержит собственную цепочку:

  • transform
  • test
  • when
  • required и др.

При пересоздании эти цепочки заново компилируются, что особенно дорого при глубокой вложенности объектов.

Пример проблемной структуры:

Yup.object({
  user: Yup.object({
    profile: Yup.object({
      age: Yup.number().required()
    })
  })
});

Повторная генерация такой схемы увеличивает стоимость валидации пропорционально глубине дерева.


Стабильность ссылок как ключевой фактор оптимизации

Многие библиотеки форм и кэширования используют простое сравнение:

  • prevSchema === nextSchema

При пересоздании схемы даже с идентичной структурой это условие нарушается, что приводит к:

  • повторной инициализации форм;
  • сбросу состояния валидации;
  • лишним ререндерингам UI.

Стабильность ссылок становится важнее самой структуры схемы.


Разделение декларации и исполнения схемы

Оптимальная архитектура предполагает разделение:

  • декларации схем (создаётся один раз);
  • исполнения валидации (многократно вызывается).
const schema = Yup.object({
  email: Yup.string().required()
});

const validate = async (data) => {
  return schema.validate(data);
};

Такой подход устраняет необходимость пересоздания схемы даже при множественных вызовах валидации.