При построении форм на базе react-hook-form с
использованием YupResolver ключевая сложность возникает в
момент подключения нестандартных UI-компонентов. Большинство современных
интерфейсных библиотек (MUI, Ant Design, Chakra UI) предоставляют
контролируемые компоненты, которые не всегда напрямую совместимы с
регистрацией через register. В таких случаях требуется
использование промежуточного слоя синхронизации состояния формы и
внешнего компонента.
Механизм register ориентирован на нативные DOM-элементы
и неконтролируемые компоненты. Он предполагает, что значение поля
извлекается напрямую из события onChange и хранится внутри
внутреннего реестра формы.
Однако кастомные компоненты часто:
event.target.valuevalueВ таких условиях register становится недостаточным, и
применяется Controller.
Controller обеспечивает унифицированный слой
взаимодействия между системой валидации и кастомным UI. Он принимает
управление значением поля и синхронизирует его с состоянием формы.
Типовая структура интеграции:
<Controller
name="user.email"
control={control}
render={({ field, fieldState }) => (
<CustomInput
value={field.value}
onCha nge={field.onChange}
onB lur={field.onBlur}
error={fieldState.error?.message}
/>
)}
/>
Здесь field содержит:
value — текущее значение из состояния формыonChange — функция обновления значенияonBlur — обработчик потери фокусаfieldState предоставляет доступ к результатам валидации,
полученным через YupResolver.
YupResolver выполняет преобразование схемы Yup в формат,
понятный системе валидации react-hook-form. При каждом
изменении значения поля происходит:
Для кастомных компонентов важно, что YupResolver
работает не с DOM, а с финальной моделью данных формы. Это означает, что
корректность зависит от того, насколько точно компонент передаёт
значение в форму.
Кастомные компоненты часто работают не со строками, а со сложными структурами:
{
name: "address",
value: {
city: "",
street: "",
zip: ""
}
}
Схема Yup должна соответствовать структуре данных:
const schema = yup.object({
address: yup.object({
city: yup.string().required(),
street: yup.string().required(),
zip: yup.string().matches(/^\d+$/)
})
});
При использовании Controller важно передавать полностью
согласованную структуру:
<Controller
name="address"
control={control}
render={({ field }) => (
<AddressInput
value={field.value}
onCha nge={field.onChange}
/>
)}
/>
Любое частичное обновление объекта должно выполняться явно, иначе
YupResolver будет получать неполные данные и генерировать
ошибки валидации.
Работа с кастомными компонентами усложняется при использовании массивов. Типичный пример — список тегов, динамических инпутов или повторяющихся блоков.
const schema = yup.object({
tags: yup.array().of(yup.string().required())
});
При использовании useFieldArray обеспечивается
синхронизация индексов и значений:
const { fields, append, remove } = useFieldArray({
control,
name: "tags"
});
Интеграция с кастомным компонентом:
fields.map((field, index) => (
<Controller
key={field.id}
name={`tags.${index}`}
control={control}
render={({ field }) => (
<TagInput
value={field.value}
onCha nge={field.onChange}
/>
)}
/>
));
Ключевой аспект — стабильная идентификация элементов через
field.id, так как YupResolver реагирует на
изменения массива как на изменение структуры данных целиком.
Кастомные компоненты часто возвращают значения, не совпадающие с ожидаемым форматом схемы. В таких случаях применяется трансформация:
const schema = yup.object({
price: yup
.number()
.transform((value, originalValue) =>
typeof originalValue === "string"
? parseFloat(originalValue.replace(",", "."))
: value
)
});
Это особенно важно для:
YupResolver выполняет валидацию уже после применения
трансформаций, что позволяет унифицировать поведение кастомных
компонентов без дополнительной логики в UI.
Некоторые кастомные компоненты используют внешний state manager
(Redux, Zustand, MobX). В таких случаях возникает дублирование
источников истины. Для предотвращения рассинхронизации применяется явная
привязка через setValue:
useEffect(() => {
setValue("profile", externalProfileData);
}, [externalProfileData]);
При этом YupResolver будет валидировать уже обновлённое
значение формы.
Важно учитывать, что частые вызовы setValue могут
приводить к повторным вычислениям схемы, особенно при сложных
Yup-валидаторах.
Кастомные компоненты часто управляют условной логикой отображения полей. Это приводит к необходимости динамической схемы Yup:
const schema = yup.object({
hasCompany: yup.boolean(),
companyName: yup.string().when("hasCompany", {
is: true,
then: schema => schema.required()
})
});
В связке с Controller это позволяет синхронизировать
визуальное состояние и правила валидации без дополнительной логики
внутри компонентов.
YupResolver возвращает ошибки в формате, который
соответствует структуре формы:
{
address: {
city: {
message: "Обязательное поле"
}
}
}
При работе с кастомными компонентами важно правильно извлекать ошибку:
error={errors?.address?.city?.message}
Для унификации часто применяется вспомогательный слой доступа к ошибкам, особенно при глубоко вложенных структурах.
Кастомные компоненты могут стать источником лишних ререндеров. Основные причины:
render в
Controllervaluemode: "onChange"Оптимизация достигается через:
React.memo для компонентовuseCallback)onBlur вместо
onChange)При использовании масок ввода (телефон, дата, валюта) кастомный компонент часто преобразует значение до отображения. В этом случае важно разделять:
<MaskedInput
value={field.value}
onCha nge={(val) => field.onChange(unmask(val))}
/>
YupResolver в этом случае работает исключительно с
“чистым” значением, исключая влияние форматирования.
При глубокой вложенности компонентов формируется иерархия контроллеров, где каждый уровень отвечает за собственную часть структуры данных. Это особенно актуально для форм типа:
Каждый уровень должен сохранять соответствие Yup-схеме, иначе валидация становится частичной и непредсказуемой.
const schema = yup.object({
profile: yup.object({
contacts: yup.object({
phone: yup.string().required()
})
})
});
<Controller
name="profile.contacts.phone"
control={control}
render={({ field }) => (
<PhoneInput
value={field.value}
onCha nge={field.onChange}
/>
)}
/>