Формы с повторяющимися группами полей

Формы с повторяющимися группами полей представляют собой структуру, в которой один и тот же набор значений может существовать в нескольких экземплярах. Типичный пример — список адресов пользователя, перечень товаров в заказе, набор контактов или этапов процесса. В таких сценариях данные естественным образом выражаются через массив объектов.

В контексте работы с YupResolver и react-hook-form такие структуры требуют согласованного описания схемы валидации и корректного связывания с динамическими полями формы.


Модель данных для повторяющихся групп

Основой выступает массив объектов. Каждый объект описывает одну группу полей:

const defaultValues = {
  contacts: [
    {
      name: "",
      email: "",
      phone: ""
    }
  ]
};

Структура фиксирует ключевую особенность: форма не оперирует отдельными независимыми полями, а управляет коллекцией однотипных сущностей.


Схема валидации Yup для массивов объектов

Библиотека Yup предоставляет специализированный API для описания массивов через yup.array().of(yup.object()).

import * as yup from "yup";

const contactSchema = yup.object({
  name: yup.string().required("Имя обязательно"),
  email: yup.string().email("Некорректный email").required("Email обязателен"),
  phone: yup.string().min(10, "Минимум 10 символов")
});

const schema = yup.object({
  contacts: yup.array().of(contactSchema)
    .min(1, "Должен быть хотя бы один контакт")
});

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


Подключение YupResolver к react-hook-form

YupResolver выступает связующим слоем между схемой Yup и механизмом валидации react-hook-form.

import { useForm, useFieldArray } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";

const form = useForm({
  defaultValues,
  resolver: yupResolver(schema)
});

При каждом изменении данных форма проходит через резолвер, который сопоставляет структуру значений с описанной схемой и формирует объект ошибок.


Использование useFieldArray для динамических групп

Для работы с повторяющимися блоками используется useFieldArray, который управляет добавлением и удалением элементов массива.

const { fields, append, remove } = useFieldArray({
  control: form.control,
  name: "contacts"
});

Каждый элемент fields соответствует одной группе полей и содержит стабильный идентификатор, необходимый для корректного рендеринга.


Рендеринг динамических групп полей

Структура UI строится через итерацию по массиву fields.

return (
  <form onSub mit={form.handleSubmit(onSubmit)}>
    {fields.map((field, index) => (
      <div key={field.id}>
        <input
          {...form.register(`contacts.${index}.name`)}
          placeholder="Имя"
        />

        <input
          {...form.register(`contacts.${index}.email`)}
          placeholder="Email"
        />

        <input
          {...form.register(`contacts.${index}.phone`)}
          placeholder="Телефон"
        />

        <button type="button" onCl ick={() => remove(index)}>
          Удалить
        </button>
      </div>
    ))}

    <button
      type="button"
      onCl ick={() => append({ name: "", email: "", phone: "" })}
    >
      Добавить контакт
    </button>

    <button type="submit">Сохранить</button>
  </form>
);

Сопоставление ошибок Yup с индексами массива

При использовании YupResolver ошибки формируются в иерархической структуре, отражающей путь к полю:

errors.contacts?.[0]?.email

или при динамическом доступе:

errors.contacts?.[index]?.name

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


Расширенные ограничения массива

В Yup доступны дополнительные ограничения для управления структурой коллекции:

const schema = yup.object({
  contacts: yup.array()
    .of(contactSchema)
    .min(1, "Минимум один элемент")
    .max(5, "Максимум пять элементов")
    .required()
});

Также возможно добавление пользовательских проверок:

contacts: yup.array().test(
  "unique-emails",
  "Email должен быть уникальным",
  (value) => {
    const emails = value?.map(v => v.email);
    return new Set(emails).size === emails.length;
  }
)

Вложенные группы внутри повторяющихся структур

Повторяющиеся группы часто содержат вложенные массивы, формируя многоуровневую модель.

const schema = yup.object({
  orders: yup.array().of(
    yup.object({
      title: yup.string().required(),
      items: yup.array().of(
        yup.object({
          name: yup.string().required(),
          quantity: yup.number().min(1)
        })
      )
    })
  )
});

В таких структурах путь ошибки становится глубже:

orders[0].items[2].quantity

Динамическое добавление сложных объектов

Добавление вложенных структур требует полного соответствия default-объекту:

append({
  title: "",
  items: [
    {
      name: "",
      quantity: 1
    }
  ]
});

Несоответствие структуры приводит к рассинхронизации между react-hook-form и Yup-схемой.


Перерендеринг и производительность

При работе с массивами полей важно учитывать стоимость обновлений состояния. Каждый append или remove инициирует перерасчёт всей формы. Оптимизация достигается за счёт:

  • использования стабильных key через field.id
  • минимизации глубины вложенности
  • избегания лишних подписок на состояние формы

Частые ошибки при работе с повторяющимися группами

Использование индекса массива как ключа рендера приводит к некорректному сопоставлению значений при удалении элементов.

// Нежелательный подход
fields.map((field, index) => (
  <div key={index}>

Корректный вариант основан на field.id, предоставляемом useFieldArray.


Неконсистентность структуры defaultValues и схемы Yup приводит к отсутствию валидации или невозможности корректного отображения ошибок. Особое внимание требуется при наличии вложенных массивов и условных полей.


Условная валидация внутри повторяющихся групп

Yup позволяет задавать зависимости между полями внутри одного объекта массива:

const contactSchema = yup.object({
  type: yup.string().required(),
  value: yup.string().when("type", {
    is: "email",
    then: schema => schema.email().required(),
    otherwise: schema => schema.required()
  })
});

Такая конструкция сохраняет целостность логики внутри каждого элемента массива независимо от остальных.