Интеграция Yup через yupResolver чаще всего
используется совместно с библиотекой React Hook Form. Несмотря на
высокую производительность этой связки, неправильная организация схем
валидации и структуры формы может приводить к большому количеству
повторных рендеров компонентов.
Основные причины:
Yup на каждом рендере;formState;watch() без ограничений;object, array,
function);yupResolver создаёт адаптер между схемой
Yup и механизмом валидации
React Hook Form.
Пример:
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
const schema = yup.object({
email: yup.string().email().required(),
password: yup.string().min(6).required()
});
const form = useForm({
resolver: yupResolver(schema)
});
Во время изменения поля:
React Hook Form вызывает resolver;schema.validate();formState;Чем меньше изменений происходит в formState, тем меньше
ре-рендеров.
Неправильный вариант:
function LoginForm() {
const schema = yup.object({
email: yup.string().required()
});
const form = useForm({
resolver: yupResolver(schema)
});
return <form />;
}
На каждом рендере:
React Hook Form считает конфигурацию изменённой;Оптимальный вариант:
const schema = yup.object({
email: yup.string().required()
});
function LoginForm() {
const form = useForm({
resolver: yupResolver(schema)
});
return <form />;
}
Преимущества:
Иногда схема зависит от параметров.
Например:
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 />;
}
Без useMemo схема будет пересоздаваться постоянно.
Плохой пример:
useForm({
resolver: yupResolver(
yup.object({
name: yup.string().required()
})
)
});
Здесь одновременно создаются:
const schema = yup.object({
name: yup.string().required()
});
const resolver = yupResolver(schema);
function App() {
const form = useForm({
resolver
});
return <form />;
}
Это особенно важно в крупных формах с большим количеством полей.
formState — реактивный объект.
При изменении:
errors;dirtyFields;touchedFields;isValid;isSubmitting;происходит обновление подписанных компонентов.
const {
formState
} = useForm();
Или:
const {
formState: { errors, isDirty, isValid }
} = useForm();
Компонент подписывается сразу на несколько состояний.
Правильный подход:
const { control } = useForm();
const { errors } = useFormState({
control,
name: "email"
});
Теперь компонент реагирует только на изменения поля
email.
const values = watch();
Такой вызов подписывает компонент на изменения всей формы.
Каждый ввод символа вызывает:
const email = watch("email");
Или:
const [email, password] = watch([
"email",
"password"
]);
Количество обновлений становится значительно меньше.
useWatch изолирует ререндеры.
const email = useWatch({
control,
name: "email"
});
Преимущество:
useWatch;Большой компонент:
function Form() {
return (
<>
<input />
<input />
<input />
<input />
</>
);
}
При изменении одного поля может обновиться весь компонент.
const EmailField = memo(() => {
const { register } = useFormContext();
return <input {...register("email")} />;
});
Теперь ререндер ограничивается конкретным полем.
const InputField = React.memo(function InputField({
register,
name
}) {
return <input {...register(name)} />;
});
Если пропсы не изменились:
Антипаттерн:
<InputField rules={{ required: true }} />
На каждом рендере создаётся новый объект.
Правильнее:
const rules = useMemo(() => ({
required: true
}), []);
<InputField rules={rules} />
useForm({
mode: "onChange"
});
onChange запускает валидацию при каждом вводе
символа.
Это самый тяжёлый режим.
useForm({
mode: "onBlur"
});
Валидация запускается после потери фокуса.
useForm({
mode: "onSubmit"
});
Самый дешёвый режим:
useForm({
mode: "onSubmit",
reValidateMode: "onBlur"
});
Хороший баланс между UX и производительностью.
Yup может собирать все ошибки формы:
schema.validate(data, {
abortEarly: false
});
Это дороже по вычислениям.
schema.validate(data, {
abortEarly: true
});
Преимущества:
Особенно заметно в больших формах.
yup.object({
type: yup.string(),
data: yup.object().when("type", {
is: "admin",
then: heavyAdminSchema
})
});
Сложные ветки могут вычисляться слишком часто.
const schema = yup.object({
type: yup.string(),
data: yup.lazy((value) => {
if (value?.type === "admin") {
return adminSchema;
}
return userSchema;
})
});
Преимущества:
yup.string().test(
"email-check",
"Email already exists",
async (value) => {
const res = await api.checkEmail(value);
return res.ok;
}
);
При каждом вводе:
isValidating;const debouncedCheck = debounce(async (value) => {
return api.checkEmail(value);
}, 500);
Преимущества:
Массивы полей особенно чувствительны к ререндерам.
const { fields } = useFieldArray({
control,
name: "users"
});
При обновлении одного элемента может обновляться весь список.
Правильно:
{
fields.map((field) => (
<UserRow
key={field.id}
field={field}
/>
));
}
Неправильно:
key={index}
const UserRow = memo(function UserRow({
field
}) {
return <input defaultValue={field.name} />;
});
Это критически важно для больших таблиц.
React Hook Form изначально оптимизирован под
неконтролируемые поля.
<input {...register("email")} />
Такой подход:
<Controller
control={control}
name="email"
render={({ field }) => (
<Input {...field} />
)}
/>
Controller делает компонент контролируемым.
Это увеличивает:
Использование допустимо для:
Во всех остальных случаях register()
производительнее.
Основной инструмент анализа:
Библиотека помогает выявлять:
Пример:
InputField.whyDidYouRender = true;
Плохо:
<MassiveForm />
Лучше:
<UserSection />
<AddressSection />
<SettingsSection />
const AdminSection = lazy(() => import("./AdminSection"));
Преимущества:
step === 1 && <StepOne />
step === 2 && <StepTwo />
Это уменьшает:
const emailSchema = yup.string()
.email()
.required();
const schema = yup.object({
email: emailSchema
});
Преимущества:
Плохо:
yup.object({
a: yup.object({
b: yup.object({
c: yup.object({
d: yup.string()
})
})
})
});
Глубокие структуры:
const schema = yup.object({
email: yup.string()
.email()
.required(),
password: yup.string()
.min(6)
.required()
});
const resolver = yupResolver(schema);
const EmailField = memo(() => {
const { register } = useFormContext();
return (
<input
{...register("email")}
/>
);
});
const PasswordField = memo(() => {
const { register } = useFormContext();
return (
<input
type="password"
{...register("password")}
/>
);
});
function LoginForm() {
const methods = useForm({
resolver,
mode: "onBlur"
});
return (
<FormProvider {...methods}>
<form>
<EmailField />
<PasswordField />
</form>
</FormProvider>
);
}
Особенности оптимизации:
onBlur;useMemo;watch();React.memo;Controller;onChange при тяжёлой валидации;| Источник | Стоимость |
|---|---|
watch() без аргументов |
Очень высокая |
mode: "onChange" |
Очень высокая |
| Async validation | Высокая |
| Controller-компоненты | Высокая |
| Глубокие Yup-схемы | Высокая |
| Пересоздание schema | Средняя |
| Глобальный formState | Средняя |
| Большие FieldArray | Средняя |
В крупных приложениях формы могут содержать:
В таких сценариях оптимизация YupResolver становится обязательной частью архитектуры.
Типичный стек оптимизации:
React.memo;useWatch;FormProvider;useFormContext;Даже небольшое сокращение числа ререндеров в таких формах может уменьшить время отклика интерфейса в несколько раз.