При работе с крупными формами в интерфейсах часто возникает необходимость перехода от разрозненной ручной валидации к централизованной схеме на основе Yup и интеграции через YupResolver в связке с React Hook Form. Полная замена подхода «за один шаг» в больших кодовых базах приводит к регрессиям, сложностям тестирования и непредсказуемым побочным эффектам. Поэтому используется стратегия постепенной миграции, при которой форма переводится на схему частями, сохраняя работоспособность на каждом этапе.
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({
firstName: yup.string().required(),
age: yup.number().min(18),
});
const form = useForm({
resolver: yupResolver(schema),
});
В такой конфигурации вся валидация централизуется в схеме. Однако при миграции больших форм этот подход редко применяется сразу ко всей форме, поскольку существующий код часто содержит:
Основной принцип миграции заключается в том, чтобы YupResolver применялся не ко всей форме, а к её изолированным частям.
Большие формы логически делятся на независимые блоки:
Каждый блок выделяется в отдельную Yup-схему:
const userSchema = yup.object({
firstName: yup.string().required(),
lastName: yup.string().required(),
});
const addressSchema = yup.object({
city: yup.string().required(),
zip: yup.string().matches(/^\d{6}$/),
});
На этапе миграции используется объединение:
const fullSchema = yup.object({
...userSchema.fields,
...addressSchema.fields,
});
Такой подход позволяет постепенно переносить правила без изменения структуры формы.
Во многих проектах форма уже содержит ручные проверки:
if (!values.email.includes("@")) {
setError("email", "Invalid email");
}
Переход осуществляется поэтапно:
Старая проверка остаётся активной.
email: yup.string().email("Invalid email"),
Временное дублирование позволяет выявить расхождения между старой и новой логикой.
После стабилизации поведения старая логика удаляется.
React Hook Form позволяет запускать валидацию по событиям:
onChangeonBluronSubmitПри миграции больших форм часто используется гибридный режим:
useForm({
resolver: yupResolver(schema),
mode: "onBlur",
reValidateMode: "onChange",
});
Такой режим снижает нагрузку на частично мигрированные формы, где не все поля ещё описаны в Yup.
Yup позволяет использовать необязательные поля для временного сосуществования старой и новой логики.
const schema = yup.object({
firstName: yup.string().required(),
lastName: yup.string().notRequired(),
legacyField: yup.mixed().notRequired(),
});
Ключевой принцип: каждое новое поле добавляется в схему, не нарушая работу существующих данных.
Большие формы часто содержат динамически добавляемые блоки. В этом
случае используется lazy:
const dynamicSchema = yup.lazy((value) => {
if (value?.type === "company") {
return yup.object({
companyName: yup.string().required(),
vat: yup.string().required(),
});
}
return yup.object({
personalId: yup.string().required(),
});
});
При миграции важно не переносить всю динамику сразу, а вводить её по сценариям использования.
При поэтапном переходе часто возникает ситуация, когда часть формы использует Yup, а часть — legacy-валидацию. Тогда используется составной резолвер:
const resolver = async (values, context, options) => {
const yupResult = await yupResolver(partialSchema)(values, context, options);
const legacyErrors = legacyValidate(values);
return {
values,
errors: {
...yupResult.errors,
...legacyErrors,
},
};
};
Такой подход позволяет избежать «двойного переписывания» всей формы.
Самый управляемый способ — перенос логики по одному полю.
const schema = yup.object({
email: yup.string().email().required(),
});
Остальные поля остаются вне YupResolver и валидируются вручную.
Постепенно добавляются новые поля:
const schema = yup.object({
email: yup.string().email().required(),
phone: yup.string().matches(/^\+?\d+$/),
});
Такой подход минимизирует риск регрессий.
При частичной миграции важно унифицировать формат ошибок.
Yup возвращает структуру:
{
path: "email",
message: "Invalid email"
}
Legacy-валидация часто возвращает:
{
email: "Invalid email"
}
Для объединения используется адаптер:
const normalizeErrors = (errors) => {
return Object.entries(errors).reduce((acc, [key, value]) => {
acc[key] = {
type: "manual",
message: value.message || value,
};
return acc;
}, {});
};
Большие формы часто содержат поля с зависимостями:
В Yup это выражается через when:
confirmPassword: yup
.string()
.oneOf([yup.ref("password")], "Passwords must match"),
При миграции такие зависимости переносятся в последнюю очередь, поскольку они чаще всего связаны с несколькими частями UI одновременно.
Переход на YupResolver может увеличить стоимость валидации, если схема становится слишком большой.
Применяются следующие техники:
const resolver = yupResolver(partialSchemaA);
и подключение через композицию только активных блоков формы.
Схема активируется только при необходимости:
const schema = useMemo(() => {
if (step === 2) return step2Schema;
return step1Schema;
}, [step]);
Схемы выносятся за пределы компонента, чтобы избежать повторной инициализации.
При постепенной миграции критично тестировать не только финальную схему, но и промежуточные состояния.
Проверяются:
Пример теста:
expect(yupResult.errors.email).toEqual(legacyResult.email);
В TypeScript часто возникает конфликт между частично описанной схемой и полной моделью формы.
Используется стратегия постепенного расширения типов:
type FormValues = {
email: string;
// временно необязательные поля
phone?: string;
};
После полной миграции типы синхронизируются с Yup:
type FormValues = yup.InferType<typeof schema>;
Основные риски:
Для контроля используются:
В wizard-формах миграция выполняется по шагам:
const schemas = [
step1Schema,
step2Schema,
step3Schema,
];
Каждый шаг получает свой resolver:
resolver: yupResolver(schemas[currentStep])
Такой подход позволяет мигрировать шаги независимо друг от друга без влияния на общую структуру формы.
При постепенном переходе важно избегать ситуации, когда UI уже изменён, а схема ещё нет.
Используется единая точка управления:
Постепенно legacy-слой сокращается до полного исчезновения, а YupResolver становится единственным механизмом валидации.