Вложенные формы являются ключевым инструментом при работе со сложными структурами данных, когда форма выходит за пределы плоского набора полей и начинает включать объекты, массивы, списки и динамические секции. В экосистеме React-форм наиболее распространённый стек для валидации таких структур — сочетание Yup и YupResolver (из @hookform/resolvers), обеспечивающее декларативную схему валидации и её интеграцию с react-hook-form.
Работа с вложенными формами требует понимания трёх уровней: структуры данных, схемы Yup и механизма резолвера, который сопоставляет ошибки с конкретными полями формы.
Вложенная форма в JavaScript обычно представляет собой объект, содержащий вложенные объекты и массивы:
const defaultValues = {
user: {
name: '',
contacts: {
email: '',
phone: ''
}
},
addresses: [
{
city: '',
street: '',
zip: ''
}
]
}
Такая структура отражает реальный кейс: пользователь с контактами и списком адресов. Основная сложность заключается в корректной валидации глубоко вложенных полей и синхронизации ошибок с UI.
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 выступает связующим звеном между 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.nameuser.contacts.emailaddresses.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
При построении сложных форм важно, чтобы 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 обязателен')
})
})
Вложенные условия позволяют создавать сложную бизнес-логику без необходимости писать внешние валидаторы.
Несовпадение структуры схемы и формы
Если форма использует user.contacts.email, а схема
описывает contacts.email без user, валидация
перестаёт работать корректно.
Потеря индексов в массиве
Удаление элементов массива без использования
useFieldArray может приводить к рассинхронизации индексов и
ошибок.
Отсутствие defaultValues
Без полного зеркала структуры defaultValues вложенные
поля могут становиться uncontrolled, что ломает предсказуемость
формы.
При увеличении глубины вложенности возрастает стоимость рендера и валидации. Используются следующие подходы:
React.memo)useMemoТипы позволяют избежать несоответствий структуры:
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')
Это упрощает доступ к ошибкам в динамических интерфейсах.
В реальных системах вложенные формы часто включают:
YupResolver обеспечивает единый механизм валидации, но архитектурно важно:
yup.object().shape()const schema = yup.object({
user: userSchema,
addresses: addressesSchema
})
Такой подход улучшает повторное использование и тестируемость логики валидации.