Мемоизация схем валидации

При работе с Yup и yupResolver в приложениях на React часто создаются сложные схемы валидации, содержащие:

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

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

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

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


Почему схема Yup пересоздаётся

В React любой код внутри компонента выполняется заново при каждом рендере.

Пример:

const schema = yup.object({
  email: yup.string().email().required(),
  password: yup.string().min(8).required()
});

Если схема объявлена внутри компонента:

function LoginForm() {
  const schema = yup.object({
    email: yup.string().email().required(),
    password: yup.string().min(8).required()
  });

  const form = useForm({
    resolver: yupResolver(schema)
  });

  return <form />;
}

то при каждом рендере:

  1. создаётся новый объект схемы;
  2. создаётся новый resolver;
  3. React Hook Form получает новые ссылки;
  4. часть внутренней оптимизации перестаёт работать.

Даже если структура схемы не изменилась, ссылка на объект уже новая.


Как работает мемоизация

Мемоизация сохраняет результат вычисления.

В React для этого используется useMemo.

const schema = useMemo(() => {
  return yup.object({
    email: yup.string().email().required(),
    password: yup.string().min(8).required()
  });
}, []);

Теперь схема создаётся только один раз.


Базовая мемоизация yupResolver

Без мемоизации

function RegisterForm() {
  const schema = yup.object({
    name: yup.string().required(),
    age: yup.number().required()
  });

  const form = useForm({
    resolver: yupResolver(schema)
  });

  return <form />;
}

С мемоизацией схемы

function RegisterForm() {
  const schema = useMemo(() => {
    return yup.object({
      name: yup.string().required(),
      age: yup.number().required()
    });
  }, []);

  const form = useForm({
    resolver: yupResolver(schema)
  });

  return <form />;
}

Мемоизация resolver

Часто мемоизируют не только схему, но и сам resolver.

function RegisterForm() {
  const schema = useMemo(() => {
    return yup.object({
      name: yup.string().required(),
      age: yup.number().required()
    });
  }, []);

  const resolver = useMemo(() => {
    return yupResolver(schema);
  }, [schema]);

  const form = useForm({
    resolver
  });

  return <form />;
}

Такой подход особенно полезен:

  • в крупных формах;
  • при передаче resolver вниз по дереву;
  • при использовании кастомных хуков;
  • в сложных UI-конструкциях.

Когда мемоизация действительно важна

Простые формы

Для формы из 2–3 полей разница может быть незаметной.

Большие формы

Важность резко возрастает при наличии:

  • десятков полей;
  • массивов;
  • вложенных объектов;
  • when;
  • lazy;
  • динамических схем;
  • асинхронных проверок;
  • многошаговых форм.

Проблема повторных ререндеров

Каждая новая ссылка на схему считается новым объектом.

const schema = yup.object({
  title: yup.string()
});

Даже если структура одинаковая:

schema1 !== schema2

React Hook Form воспринимает resolver как изменившийся.

Это может вызывать:

  • перерасчёт валидаторов;
  • повторную регистрацию;
  • сброс внутреннего кеша;
  • повторную проверку полей.

Мемоизация динамических схем

Часто схема зависит от параметров.

Например:

function ProfileForm({ isAdmin }) {
  const schema = useMemo(() => {
    return yup.object({
      name: yup.string().required(),
      role: isAdmin
        ? yup.string().required()
        : yup.string()
    });
  }, [isAdmin]);

  const form = useForm({
    resolver: yupResolver(schema)
  });

  return <form />;
}

Схема будет пересоздаваться только при изменении isAdmin.


Ошибки неправильной мемоизации

Пустой массив зависимостей при динамических данных

Неправильно:

const schema = useMemo(() => {
  return yup.object({
    limit: yup.number().max(maxValue)
  });
}, []);

Значение maxValue изменится, но схема останется старой.

Правильно:

const schema = useMemo(() => {
  return yup.object({
    limit: yup.number().max(maxValue)
  });
}, [maxValue]);

Мемоизация и условная валидация

Использование when

const schema = useMemo(() => {
  return yup.object({
    hasPhone: yup.boolean(),

    phone: yup.string().when("hasPhone", {
      is: true,
      then: schema => schema.required(),
      otherwise: schema => schema.notRequired()
    })
  });
}, []);

when уже содержит внутреннюю динамику, поэтому схема не обязана пересоздаваться на каждое изменение поля.


Использование lazy и мемоизация

lazy позволяет строить схему динамически.

const schema = useMemo(() => {
  return yup.lazy(value => {
    if (typeof value === "string") {
      return yup.string().required();
    }

    return yup.object({
      name: yup.string().required()
    });
  });
}, []);

Без мемоизации функция lazy создаётся заново при каждом рендере.


Мемоизация вложенных схем

Большие формы удобно разбивать.

Отдельные схемы

const addressSchema = yup.object({
  city: yup.string().required(),
  street: yup.string().required()
});

const userSchema = yup.object({
  name: yup.string().required(),
  address: addressSchema
});

С мемоизацией

const addressSchema = useMemo(() => {
  return yup.object({
    city: yup.string().required(),
    street: yup.string().required()
  });
}, []);

const userSchema = useMemo(() => {
  return yup.object({
    name: yup.string().required(),
    address: addressSchema
  });
}, [addressSchema]);

Вынесение схем за пределы компонента

Самый простой способ избежать пересоздания — объявить схему вне компонента.

const schema = yup.object({
  email: yup.string().email().required()
});

function LoginForm() {
  const form = useForm({
    resolver: yupResolver(schema)
  });

  return <form />;
}

Такой вариант лучше useMemo, если схема:

  • статична;
  • не зависит от props;
  • не использует состояние;
  • не использует контекст.

Что лучше: useMemo или внешняя схема

Внешняя схема

Подходит для:

  • полностью статических форм;
  • повторного использования;
  • библиотек компонентов.

useMemo

Подходит для:

  • динамических правил;
  • параметризованных форм;
  • зависимостей от props;
  • локальной конфигурации.

Кеширование фабрики схем

Иногда создают фабрику:

function createSchema(locale) {
  return yup.object({
    name: yup.string().required(locale.required)
  });
}

Использование:

const schema = useMemo(() => {
  return createSchema(locale);
}, [locale]);

Это особенно важно при:

  • мультиязычности;
  • серверных конфигурациях;
  • feature flags;
  • настройках пользователя.

Мемоизация в кастомных хуках

Пример

function useUserFormSchema(isAdmin) {
  return useMemo(() => {
    return yup.object({
      name: yup.string().required(),

      permissions: isAdmin
        ? yup.array().required()
        : yup.array()
    });
  }, [isAdmin]);
}

Использование:

const schema = useUserFormSchema(isAdmin);

const form = useForm({
  resolver: yupResolver(schema)
});

Мемоизация и производительность

Наиболее дорогие операции в Yup

Серьёзную нагрузку создают:

  • matches с тяжёлыми regex;
  • вложенные array().of(...);
  • большие when;
  • кастомные test;
  • асинхронная валидация;
  • динамическая генерация схем.

Оптимизация регулярных выражений

Плохо:

const schema = yup.object({
  password: yup.string().matches(new RegExp("..."))
});

Регулярное выражение создаётся заново.

Лучше:

const passwordRegex = /.../;

const schema = yup.object({
  password: yup.string().matches(passwordRegex)
});

Избежание глубокого пересоздания

Плохо:

const schema = useMemo(() => {
  return yup.object({
    items: yup.array().of(
      yup.object({
        title: yup.string().required()
      })
    )
  });
}, [items]);

Если items часто меняется, схема постоянно пересоздаётся.

Лучше отделять данные от структуры схемы.


Мемоизация и React Strict Mode

В development-режиме React может вызывать рендер дважды.

Без мемоизации это приводит к двойному созданию схем.

Особенно заметно при:

  • сложной генерации;
  • больших массивах;
  • асинхронной подготовке;
  • heavy validation logic.

Мемоизация и FormProvider

При использовании FormProvider пересоздание resolver может вызвать обновление множества дочерних компонентов.

<FormProvider {...form}>
  <ProfileFields />
  <AddressFields />
  <SettingsFields />
</FormProvider>

Стабильная ссылка на resolver уменьшает каскад обновлений.


Мемоизация и SSR

В Next.js и других SSR-фреймворках повторная генерация схем может происходить:

  • на сервере;
  • на клиенте;
  • при гидратации.

Стабильные схемы уменьшают вероятность:

  • лишних вычислений;
  • несовпадений;
  • проблем гидратации.

Мемоизация асинхронных тестов

Пример

const schema = useMemo(() => {
  return yup.object({
    username: yup.string().test(
      "check-user",
      "User already exists",
      async value => {
        const result = await api.checkUser(value);

        return result.available;
      }
    )
  });
}, []);

Без мемоизации асинхронный тест создаётся заново при каждом рендере.


Мемоизация сложных генераторов

Плохо

const schema = yup.object(
  fields.reduce((acc, field) => {
    acc[field.name] = yup.string().required();

    return acc;
  }, {})
);

Этот код выполняется на каждом рендере.

Лучше

const schema = useMemo(() => {
  return yup.object(
    fields.reduce((acc, field) => {
      acc[field.name] = yup.string().required();

      return acc;
    }, {})
  );
}, [fields]);

Разделение схем для оптимизации

Большие схемы лучше дробить.

Пример

const personalSchema = yup.object({
  firstName: yup.string().required(),
  lastName: yup.string().required()
});

const contactsSchema = yup.object({
  email: yup.string().email().required(),
  phone: yup.string().required()
});

Комбинация:

const schema = yup.object({
  personal: personalSchema,
  contacts: contactsSchema
});

Это упрощает:

  • повторное использование;
  • мемоизацию;
  • тестирование;
  • lazy loading.

Использование concat

concat объединяет схемы.

const baseSchema = yup.object({
  name: yup.string().required()
});

const adminSchema = yup.object({
  permissions: yup.array().required()
});

const schema = useMemo(() => {
  return isAdmin
    ? baseSchema.concat(adminSchema)
    : baseSchema;
}, [isAdmin]);

Когда мемоизация бесполезна

Мемоизация может не давать эффекта:

  • в очень маленьких формах;
  • при редких рендерах;
  • если схема уже вынесена наружу;
  • если основная нагрузка идёт не от Yup.

Антипаттерны

Создание схемы прямо в JSX

Плохо:

useForm({
  resolver: yupResolver(
    yup.object({
      email: yup.string().required()
    })
  )
});

Пересоздание regex

Плохо:

.matches(new RegExp(pattern))

Глубокая динамика без useMemo

Плохо:

const schema = buildComplexSchema(config);

Практический шаблон оптимизированной формы

const createSchema = settings => {
  return yup.object({
    email: yup.string().email().required(),

    password: settings.strict
      ? yup.string().min(12).required()
      : yup.string().min(6).required()
  });
};

function AppForm({ settings }) {
  const schema = useMemo(() => {
    return createSchema(settings);
  }, [settings]);

  const resolver = useMemo(() => {
    return yupResolver(schema);
  }, [schema]);

  const form = useForm({
    resolver,
    mode: "onChange"
  });

  return <form />;
}

Ключевые рекомендации

Для статических схем

Выносить за пределы компонента.


Для динамических схем

Использовать useMemo.


Для тяжёлых resolver

Мемоизировать отдельно.


Для больших проектов

Использовать:

  • фабрики схем;
  • композицию;
  • разделение модулей;
  • локальные хуки;
  • повторное использование частей схем.

Наиболее эффективные стратегии оптимизации

Подход Эффект
Вынос схем наружу Минимум пересозданий
useMemo для схем Стабильные ссылки
useMemo для resolver Меньше ререндеров
Разделение схем Локальная оптимизация
Фабрики схем Гибкость
Повторное использование regex Снижение нагрузки
Композиция concat Упрощение архитектуры
Lazy-схемы Динамическая эффективность