При работе с Yup и yupResolver в
приложениях на React часто создаются сложные схемы валидации,
содержащие:
Повторное создание схемы на каждом рендере компонента приводит к:
Мемоизация позволяет сохранить экземпляр схемы между рендерами и пересоздавать его только при изменении зависимостей.
В 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 />;
}
то при каждом рендере:
Даже если структура схемы не изменилась, ссылка на объект уже новая.
Мемоизация сохраняет результат вычисления.
В React для этого используется useMemo.
const schema = useMemo(() => {
return yup.object({
email: yup.string().email().required(),
password: yup.string().min(8).required()
});
}, []);
Теперь схема создаётся только один раз.
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.
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 />;
}
Такой подход особенно полезен:
Для формы из 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]);
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 позволяет строить схему динамически.
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, если схема:
Подходит для:
Подходит для:
Иногда создают фабрику:
function createSchema(locale) {
return yup.object({
name: yup.string().required(locale.required)
});
}
Использование:
const schema = useMemo(() => {
return createSchema(locale);
}, [locale]);
Это особенно важно при:
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)
});
Серьёзную нагрузку создают:
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 часто меняется, схема постоянно
пересоздаётся.
Лучше отделять данные от структуры схемы.
В development-режиме React может вызывать рендер дважды.
Без мемоизации это приводит к двойному созданию схем.
Особенно заметно при:
При использовании FormProvider пересоздание resolver
может вызвать обновление множества дочерних компонентов.
<FormProvider {...form}>
<ProfileFields />
<AddressFields />
<SettingsFields />
</FormProvider>
Стабильная ссылка на resolver уменьшает каскад обновлений.
В 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
});
Это упрощает:
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]);
Мемоизация может не давать эффекта:
Плохо:
useForm({
resolver: yupResolver(
yup.object({
email: yup.string().required()
})
)
});
Плохо:
.matches(new RegExp(pattern))
Плохо:
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.
Мемоизировать отдельно.
Использовать:
| Подход | Эффект |
|---|---|
| Вынос схем наружу | Минимум пересозданий |
| useMemo для схем | Стабильные ссылки |
| useMemo для resolver | Меньше ререндеров |
| Разделение схем | Локальная оптимизация |
| Фабрики схем | Гибкость |
| Повторное использование regex | Снижение нагрузки |
| Композиция concat | Упрощение архитектуры |
| Lazy-схемы | Динамическая эффективность |