Использование с Controller

В экосистеме React Hook Form валидация и управление контролируемыми компонентами решаются через два ключевых механизма: resolver (в частности yupResolver) и компонент Controller. Их совместное использование необходимо в ситуациях, когда форма содержит как стандартные HTML-элементы, так и сложные контролируемые UI-компоненты, не совместимые с register.

yupResolver обеспечивает декларативную схему валидации на основе Yup, а Controller выступает адаптером между React Hook Form и внешними контролируемыми компонентами, обеспечивая синхронизацию состояния.


Базовая интеграция React Hook Form с yupResolver и Controller

Основой является подключение 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 при работе с кастомными компонентами

Controller необходим в случаях, когда компонент:

  • не поддерживает ref
  • не использует стандартное событие onChange
  • хранит внутреннее состояние (controlled component)
  • реализован сторонней библиотекой (MUI, Ant Design, React Sel ect)

Пример с условно кастомным компонентом:

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.


Схема валидации Yup и её влияние на 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.


Синхронизация ошибок между 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).


Поведение Controller при режиме валидации

React Hook Form поддерживает несколько режимов:

  • onSubmit
  • onChange
  • onBlur
  • all

При использовании yupResolver поведение меняется только в точке триггера, но логика схемы остаётся неизменной.

useForm({
  resolver: yupResolver(schema),
  mode: "onChange",
});

Controller в этом случае будет обновлять значения при каждом изменении, что немедленно запускает повторную валидацию.


Типизация TypeScript при совместном использовании

При использовании 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-схемы.


Контролируемые компоненты сторонних библиотек

Пример с React Select

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(),
});

Пример с Material UI TextField

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

Controller может вызывать лишние рендеры при неправильной архитектуре формы. Основные принципы оптимизации:

  • избегание анонимных функций вне render
  • использование defaultValues
  • минимизация подписок через useFormState
  • разделение формы на подкомпоненты

Пример выделения поля:

const EmailField = ({ control, errors }) => (
  <Controller
    name="email"
    control={control}
    render={({ field }) => (
      <input {...field} />
    )}
  />
);

Ошибки интеграции yupResolver и Controller и их причины

1. Несовпадение defaultValues и схемы

Если defaultValues не соответствует Yup-схеме, возможны неконтролируемые undefined.

defaultValues: {
  user: {
    name: "",
  },
}

Схема должна совпадать по структуре.


2. Потеря значения при неправильном onChange

onCha nge={(value) => field.onChange(value.value)}

При этом Yup ожидает объект, но получает строку.


3. Несоответствие типов в сложных объектах

Controller передаёт значение как есть, поэтому Yup строго проверяет структуру объекта.


4. Отсутствие control в глубоко вложенных компонентах

При передаче 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.


Особенности жизненного цикла значений Controller при YupResolver

  • начальное значение берётся из defaultValues
  • при первом рендере происходит синхронизация с resolver
  • при изменении значения Controller обновляет form state
  • yupResolver пересчитывает ошибки на основе текущего состояния

Вся цепочка строго однонаправленная: UI → Controller → RHF state → YupResolver → errors → UI.