В экосистеме React Hook Form валидация и управление контролируемыми
компонентами решаются через два ключевых механизма:
resolver (в частности yupResolver) и компонент
Controller. Их совместное использование необходимо в
ситуациях, когда форма содержит как стандартные HTML-элементы, так и
сложные контролируемые UI-компоненты, не совместимые с
register.
yupResolver обеспечивает декларативную схему валидации
на основе Yup, а Controller выступает адаптером между React
Hook Form и внешними контролируемыми компонентами, обеспечивая
синхронизацию состояния.
Основой является подключение resolver к useForm, после
чего Controller начинает работать в рамках общей системы
валидации.
import { useForm, Controller } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
const schema = yup.object({
email: yup.string().email("Некорректный email").required("Обязательное поле"),
age: yup.number().min(18, "Минимальный возраст 18"),
});
export default function Form() {
const {
control,
handleSubmit,
formState: { errors },
} = useForm({
resolver: yupResolver(schema),
defaultValues: {
email: "",
age: 18,
},
});
return (
<form onSub mit={handleSubmit(console.log)}>
<Controller
name="email"
control={control}
render={({ field }) => (
<input {...field} placeholder="Email" />
)}
/>
<Controller
name="age"
control={control}
render={({ field }) => (
<input {...field} type="number" />
)}
/>
<button type="submit">Отправить</button>
</form>
);
}
Ключевой момент заключается в том, что Controller не
выполняет валидацию самостоятельно — он лишь передаёт значения в систему
формы, где yupResolver уже применяет схему.
Controller необходим в случаях, когда компонент:
refonChangeПример с условно кастомным компонентом:
function CustomInput({ value, onChange }) {
return (
<div>
<input
value={value}
onCha nge={(e) => onChange(e.target.value.toUpperCase())}
/>
</div>
);
}
Интеграция через Controller:
<Controller
name="code"
control={control}
render={({ field }) => (
<CustomInput
value={field.value}
onCha nge={field.onChange}
/>
)}
/>
В этом сценарии yupResolver не зависит от типа
компонента. Он получает финальные значения после обработки
Controller.
yupResolver преобразует Yup-схему в формат, понятный
React Hook Form. Важно учитывать, что:
Controller лишь доставляет значения в state формыformState.errorsПример более сложной схемы:
const schema = yup.object({
username: yup.string().required(),
password: yup.string().min(8),
profile: yup.object({
firstName: yup.string().required(),
lastName: yup.string().required(),
}),
});
Использование с вложенными Controller:
<Controller
name="profile.firstName"
control={control}
render={({ field }) => <input {...field} />}
/>
<Controller
name="profile.lastName"
control={control}
render={({ field }) => <input {...field} />}
/>
YupResolver автоматически сопоставляет вложенные структуры с путями
Controller.
Ошибки формируются на уровне resolver и попадают в
formState.errors. Controller не хранит ошибки
локально.
Типичный доступ:
errors.email?.message
Использование с Controller:
<Controller
name="email"
control={control}
render={({ field }) => (
<div>
<input {...field} />
{errors.email && <span>{errors.email.message}</span>}
</div>
)}
/>
Особенность: ошибка обновляется только при триггере валидации (submit, blur или change в зависимости от mode).
React Hook Form поддерживает несколько режимов:
onSubmitonChangeonBlurallПри использовании yupResolver поведение меняется только
в точке триггера, но логика схемы остаётся неизменной.
useForm({
resolver: yupResolver(schema),
mode: "onChange",
});
Controller в этом случае будет обновлять значения при
каждом изменении, что немедленно запускает повторную валидацию.
При использовании TypeScript критически важно типизировать схему и форму одновременно.
import { InferType } fr om "yup";
const schema = yup.object({
email: yup.string().required(),
age: yup.number().required(),
});
type FormValues = InferType<typeof schema>;
Далее:
const { control } = useForm<FormValues>({
resolver: yupResolver(schema),
});
И в Controller:
<Controller<FormValues>
name="email"
control={control}
render={({ field }) => <input {...field} />}
/>
Типизация гарантирует, что name соответствует структуре
Yup-схемы.
import Sel ect from "react-select";
<Controller
name="country"
control={control}
render={({ field }) => (
<Select
value={field.value}
onCha nge={field.onChange}
options={[
{ value: "kz", label: "Kazakhstan" },
{ value: "ru", label: "Russia" },
]}
/>
)}
/>
Yup-схема:
country: yup.object({
value: yup.string().required(),
label: yup.string().required(),
});
import TextField fr om "@mui/material/TextField";
<Controller
name="email"
control={control}
render={({ field }) => (
<TextField
{...field}
label="Email"
error={!!errors.email}
helperText={errors.email?.message}
/>
)}
/>
Здесь важно, что Controller полностью управляет
значением, а UI-компонент лишь отображает состояние.
Controller может вызывать лишние рендеры при
неправильной архитектуре формы. Основные принципы оптимизации:
renderdefaultValuesuseFormStateПример выделения поля:
const EmailField = ({ control, errors }) => (
<Controller
name="email"
control={control}
render={({ field }) => (
<input {...field} />
)}
/>
);
Если defaultValues не соответствует Yup-схеме, возможны
неконтролируемые undefined.
defaultValues: {
user: {
name: "",
},
}
Схема должна совпадать по структуре.
onCha nge={(value) => field.onChange(value.value)}
При этом Yup ожидает объект, но получает строку.
Controller передаёт значение как есть, поэтому Yup
строго проверяет структуру объекта.
При передаче control через пропсы возможна потеря
контекста формы, что приводит к рассинхронизации состояния.
Массивы требуют аккуратной работы с Controller:
{fields.map((item, index) => (
<Controller
key={item.id}
name={`items.${index}.name`}
control={control}
render={({ field }) => <input {...field} />}
/>
))}
Yup-схема:
items: yup.array().of(
yup.object({
name: yup.string().required(),
})
);
Каждый элемент массива синхронизируется отдельно через путь
name.
defaultValuesВся цепочка строго однонаправленная: UI → Controller → RHF state → YupResolver → errors → UI.