Вложенные формы

Вложенные формы являются ключевым инструментом при работе со сложными структурами данных, когда форма выходит за пределы плоского набора полей и начинает включать объекты, массивы, списки и динамические секции. В экосистеме React-форм наиболее распространённый стек для валидации таких структур — сочетание Yup и YupResolver (из @hookform/resolvers), обеспечивающее декларативную схему валидации и её интеграцию с react-hook-form.

Работа с вложенными формами требует понимания трёх уровней: структуры данных, схемы Yup и механизма резолвера, который сопоставляет ошибки с конкретными полями формы.


Структура вложенных данных

Вложенная форма в JavaScript обычно представляет собой объект, содержащий вложенные объекты и массивы:

const defaultValues = {
  user: {
    name: '',
    contacts: {
      email: '',
      phone: ''
    }
  },
  addresses: [
    {
      city: '',
      street: '',
      zip: ''
    }
  ]
}

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


Yup как основа описания вложенной схемы

Yup предоставляет декларативный способ описания структуры данных. Для вложенных объектов используется object(), для массивов — array().

import * as yup from 'yup'

const schema = yup.object({
  user: yup.object({
    name: yup.string().required('Имя обязательно'),
    contacts: yup.object({
      email: yup.string().email().required('Email обязателен'),
      phone: yup.string().required('Телефон обязателен')
    })
  }),
  addresses: yup.array().of(
    yup.object({
      city: yup.string().required('Город обязателен'),
      street: yup.string().required('Улица обязательна'),
      zip: yup.string().required('Индекс обязателен')
    })
  )
})

Ключевой момент: структура Yup должна полностью зеркалировать структуру формы. Любое расхождение приводит к невозможности корректного отображения ошибок.


Интеграция YupResolver

YupResolver выступает связующим звеном между Yup и react-hook-form. Он преобразует ошибки Yup в формат, понятный форме.

import { useForm } from 'react-hook-form'
import { yupResolver } from '@hookform/resolvers/yup'

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

После этого любая вложенная структура автоматически валидируется в момент submit или при изменениях (в зависимости от режима).


Доступ к вложенным полям

React Hook Form использует точечную нотацию для доступа к вложенным значениям:

  • user.name
  • user.contacts.email
  • addresses.0.city

Пример регистрации полей:

<input {...register('user.name')} />
<input {...register('user.contacts.email')} />
<input {...register('addresses.0.city')} />

Ошибка, возвращаемая YupResolver, также имеет ту же структуру ключей, что упрощает отображение сообщений.


Вложенные массивы и динамические секции

Наиболее сложный случай — массив объектов, количество которых может изменяться. Для этого используется useFieldArray.

import { useFieldArray } from 'react-hook-form'

const { fields, append, remove } = useFieldArray({
  control,
  name: 'addresses'
})

Рендер списка:

{fields.map((field, index) => (
  <div key={field.id}>
    <input {...register(`addresses.${index}.city`)} />
    <input {...register(`addresses.${index}.street`)} />
    <input {...register(`addresses.${index}.zip`)} />

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

Добавление нового элемента:

append({ city: '', street: '', zip: '' })

YupResolver корректно обрабатывает такие структуры при условии, что схема использует .array().of(object()).


Особенности валидации массивов

Массивы в Yup требуют особого внимания, поскольку ошибки могут относиться как к самому массиву, так и к его элементам.

addresses: yup.array()
  .of(
    yup.object({
      city: yup.string().required(),
      street: yup.string().required()
    })
  )
  .min(1, 'Добавьте хотя бы один адрес')

Ошибки уровня массива будут находиться в addresses.message, а ошибки элементов — в addresses[0].city.


Глубокие ошибки и их отображение

YupResolver возвращает объект ошибок, где ключи соответствуют путям формы:

{
  user: {
    contacts: {
      email: {
        message: 'Email обязателен'
      }
    }
  },
  addresses: [
    {
      city: {
        message: 'Город обязателен'
      }
    }
  ]
}

React Hook Form автоматически сопоставляет эти пути с зарегистрированными полями, но при кастомных компонентах может потребоваться ручная обработка:

const error = errors?.user?.contacts?.email?.message

Синхронизация nested-структур с UI

При построении сложных форм важно, чтобы UI отражал структуру данных:

  • каждый вложенный объект соответствует отдельному блоку интерфейса
  • массивы отображаются как повторяющиеся секции
  • вложенность не должна скрывать ошибки от пользователя

Практика разделения компонентов:

<UserSection control={control} register={register} errors={errors.user} />
<AddressesSection control={control} register={register} errors={errors.addresses} />

Такой подход снижает связность и упрощает масштабирование формы.


Условная валидация во вложенных структурах

Yup поддерживает условную валидацию через when, что особенно полезно для вложенных форм:

contacts: yup.object({
  email: yup.string().when('phone', {
    is: (phone) => !phone,
    then: (schema) => schema.required('Email или телефон обязателен')
  }),
  phone: yup.string().when('email', {
    is: (email) => !email,
    then: (schema) => schema.required('Телефон или email обязателен')
  })
})

Вложенные условия позволяют создавать сложную бизнес-логику без необходимости писать внешние валидаторы.


Частые проблемы при работе с YupResolver и вложенными формами

Несовпадение структуры схемы и формы

Если форма использует user.contacts.email, а схема описывает contacts.email без user, валидация перестаёт работать корректно.


Потеря индексов в массиве

Удаление элементов массива без использования useFieldArray может приводить к рассинхронизации индексов и ошибок.


Отсутствие defaultValues

Без полного зеркала структуры defaultValues вложенные поля могут становиться uncontrolled, что ломает предсказуемость формы.


Оптимизация больших вложенных форм

При увеличении глубины вложенности возрастает стоимость рендера и валидации. Используются следующие подходы:

  • разбиение формы на подкомпоненты
  • мемоизация секций (React.memo)
  • ленивое подключение массивов (render only when needed)
  • минимизация пересоздания схемы Yup через useMemo

Типизация вложенных форм (TypeScript)

Типы позволяют избежать несоответствий структуры:

type FormValues = {
  user: {
    name: string
    contacts: {
      email: string
      phone: string
    }
  }
  addresses: {
    city: string
    street: string
    zip: string
  }[]
}

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

const form = useForm<FormValues>({
  resolver: yupResolver(schema),
  defaultValues
})

Это обеспечивает строгую синхронизацию схемы, UI и данных.


Работа с глубоко вложенными ошибками в кастомных компонентах

В кастомных контролах часто требуется нормализация пути ошибки:

const getError = (errors, path) =>
  path.split('.').reduce((acc, key) => acc?.[key], errors)?.message

Пример:

getError(errors, 'user.contacts.email')

Это упрощает доступ к ошибкам в динамических интерфейсах.


Масштабирование вложенных форм в реальных приложениях

В реальных системах вложенные формы часто включают:

  • профили пользователей с множеством секций
  • формы заказов с товарами, адресами и оплатой
  • CRM-структуры с вложенными сущностями

YupResolver обеспечивает единый механизм валидации, но архитектурно важно:

  • держать схемы отдельно от UI
  • избегать монолитных форм-компонентов
  • использовать композицию схем через yup.object().shape()
const schema = yup.object({
  user: userSchema,
  addresses: addressesSchema
})

Такой подход улучшает повторное использование и тестируемость логики валидации.