При работе с формами в JavaScript-экосистеме, особенно в связке с
React Hook Form и Yup через @hookform/resolvers/yup,
основная сложность динамических интерфейсов заключается не в добавлении
или удалении DOM-элементов, а в согласовании структуры данных формы и
схемы валидации.
Динамические поля почти всегда реализуются через массивы объектов, где каждый элемент массива соответствует одному экземпляру повторяемой группы полей. Именно такой подход позволяет сохранять строгую типизацию и предсказуемую валидацию даже при изменении количества элементов.
Типовая структура формы с динамическими полями строится вокруг массива:
{
users: [
{ name: "", email: "" },
{ name: "", email: "" }
]
}
Каждый объект внутри массива должен иметь одинаковую структуру. Это критически важно, поскольку Yup валидирует не «форму как набор полей», а структуру данных целиком.
Базовая схема строится через array().of(object()):
import * as yup from "yup";
const schema = yup.object({
users: yup.array().of(
yup.object({
name: yup.string().required("Имя обязательно"),
email: yup.string().email("Некорректный email").required("Email обязателен")
})
)
});
Ключевой момент заключается в том, что of() определяет
правила для каждого элемента массива, а не для массива в целом.
Дополнительно можно управлять ограничениями:
users: yup.array()
.min(1, "Минимум один пользователь")
.max(10, "Максимум 10 пользователей")
YupResolver выступает адаптером между Yup-схемой и
механизмом валидации React Hook Form:
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
const form = useForm({
resolver: yupResolver(schema),
defaultValues: {
users: []
}
});
Важно понимать, что resolver не «следит» за изменениями схемы во времени. Он получает схему один раз при инициализации формы. Поэтому динамика должна решаться на уровне структуры данных, а не перестроения схемы.
Механизм динамики реализуется через useFieldArray:
import { useFieldArray } from "react-hook-form";
const { fields, append, remove } = useFieldArray({
control,
name: "users"
});
Добавление нового элемента:
append({
name: "",
email: ""
});
Удаление элемента:
remove(index);
Каждое изменение массива автоматически синхронизируется с Yup-схемой, поскольку структура данных остаётся неизменной.
Yup не требует «пересборки» схемы при добавлении или удалении элементов. Валидатор работает с актуальным значением массива в момент валидации.
Это означает:
.of()Часто требуется различать обязательные и опциональные элементы массива.
users: yup.array().of(
yup.object({
name: yup.string().required(),
email: yup.string().email()
})
)
Если необходимо разрешить пустой массив, важно не добавлять
.min(1).
Если требуется минимум один заполненный элемент:
users: yup.array()
.min(1, "Добавьте хотя бы одного пользователя")
Динамические формы часто требуют зависимых правил внутри одного элемента массива.
yup.object({
name: yup.string().required(),
email: yup.string().when("name", {
is: (val) => val && val.length > 0,
then: (schema) => schema.required("Email обязателен при наличии имени"),
otherwise: (schema) => schema.notRequired()
})
})
При использовании внутри массива контекст when
применяется к каждому элементу отдельно.
Когда структура элемента может меняться в зависимости от типа данных:
users: yup.array().of(
yup.lazy((value) => {
if (value?.type === "admin") {
return yup.object({
type: yup.string(),
permissions: yup.array().min(1)
});
}
return yup.object({
type: yup.string(),
accessLevel: yup.number().required()
});
})
)
lazy() позволяет менять схему на уровне каждого элемента
массива, что критично для сложных динамических форм.
Одна из частых ошибок — расхождение между defaultValues
и Yup-схемой:
defaultValues: {
users: undefined
}
или
defaultValues: {
users: null
}
Yup ожидает массив, поэтому безопасный вариант:
defaultValues: {
users: []
}
React Hook Form по умолчанию сохраняет значения даже после удаления поля. Это может приводить к тому, что удалённый элемент массива всё ещё участвует в валидации.
Для корректной синхронизации:
useForm({
resolver: yupResolver(schema),
shouldUnregister: true
});
Это обеспечивает удаление значений из внутреннего состояния при удалении элементов массива.
При динамическом добавлении и удалении важно учитывать момент триггера валидации:
mode: "onChange" — валидация каждого измененияmode: "onBlur" — валидация при потере фокусаmode: "onSubmit" — проверка только при отправкеuseForm({
resolver: yupResolver(schema),
mode: "onChange"
});
Основные проблемы возникают не в Yup, а в несогласованности структуры:
null вместо массиваdefaultValues для массиваfield.idReact Hook Form использует field.id для стабильной
идентификации элементов:
fields.map((field, index) => (
<input key={field.id} {...register(`users.${index}.name`)} />
))
Использование индекса как key приводит к
рассинхронизации значений при удалении элементов.
При усложнении формы массивы могут быть вложенными:
{
company: {
departments: [
{
name: "",
employees: [
{ name: "", role: "" }
]
}
]
}
}
Схема Yup строится рекурсивно:
yup.object({
company: yup.object({
departments: yup.array().of(
yup.object({
name: yup.string().required(),
employees: yup.array().of(
yup.object({
name: yup.string().required(),
role: yup.string().required()
})
)
})
)
})
})
YupResolver не пересоздаёт схему автоматически при
изменении состояния формы. Он работает как чистая функция:
values → schema → errors
Поэтому динамика формы должна быть полностью выражена через данные, а не через изменение схемы во времени.
При работе с большими массивами (50–500 элементов) важно учитывать:
validate только на submit.when() внутри каждого элементаuseMemoconst schema = useMemo(() => createSchema(), []);
Это снижает стоимость повторных вычислений resolver.
Иногда требуется валидировать только изменённый элемент:
trigger(`users.${index}`);
Это ограничивает область проверки одним объектом внутри массива, не затрагивая остальные элементы.
Удаление элемента приводит к сдвигу индексов:
0, 1, 2 → удаление 1 → 0, 1
Yup не отслеживает старые индексы, поэтому логика всегда должна опираться на текущее состояние массива, а не на сохранённые ссылки.
Иногда полезно нормализовать данные перед валидацией:
users: yup.array().transform((value) => value || [])
Это защищает от undefined и null при
неполной инициализации формы.