Работа с кастомными компонентами

При построении форм на базе react-hook-form с использованием YupResolver ключевая сложность возникает в момент подключения нестандартных UI-компонентов. Большинство современных интерфейсных библиотек (MUI, Ant Design, Chakra UI) предоставляют контролируемые компоненты, которые не всегда напрямую совместимы с регистрацией через register. В таких случаях требуется использование промежуточного слоя синхронизации состояния формы и внешнего компонента.

Контролируемые компоненты и ограничения register

Механизм register ориентирован на нативные DOM-элементы и неконтролируемые компоненты. Он предполагает, что значение поля извлекается напрямую из события onChange и хранится внутри внутреннего реестра формы.

Однако кастомные компоненты часто:

  • не используют стандартный event.target.value
  • управляют состоянием через собственные props
  • требуют явной передачи значения через value
  • используют сложные структуры данных (объекты, массивы)

В таких условиях register становится недостаточным, и применяется Controller.


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 с кастомными компонентами

YupResolver выполняет преобразование схемы Yup в формат, понятный системе валидации react-hook-form. При каждом изменении значения поля происходит:

  1. обновление состояния формы
  2. передача текущих значений в resolver
  3. выполнение Yup-схемы
  4. возврат ошибок валидации

Для кастомных компонентов важно, что 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 в Controller
  • передача новых ссылок на объекты value
  • частые триггеры валидации mode: "onChange"

Оптимизация достигается через:

  • React.memo для компонентов
  • стабилизацию обработчиков (useCallback)
  • выборочный режим валидации (onBlur вместо onChange)
  • разделение сложных форм на изолированные блоки

Двусторонняя синхронизация и внешние маски

При использовании масок ввода (телефон, дата, валюта) кастомный компонент часто преобразует значение до отображения. В этом случае важно разделять:

  • отображаемое значение
  • значение формы (source of truth)
<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}
    />
  )}
/>