Работа с формами, содержащими условные разделы, требует согласованного подхода к управлению состоянием и валидацией. Основная сложность возникает в момент, когда структура данных зависит от пользовательского выбора: одни поля должны становиться обязательными, другие — скрываться, а третьи — изменять правила проверки в зависимости от контекста.
В экосистеме React Hook Form связующим звеном между схемой валидации и формой выступает Yup Resolver, обеспечивающий синхронизацию данных формы с правилами, заданными в Yup-схеме.
Условная форма строится вокруг нескольких ключевых принципов:
Типичная структура данных:
{
userType: "individual",
individual: {
firstName: "",
lastName: ""
},
company: {
companyName: "",
vatNumber: ""
}
}
В зависимости от userType активируется либо блок
individual, либо company.
Интеграция осуществляется через resolver, который связывает схему Yup и механизм React Hook Form.
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
Базовая инициализация:
const schema = yup.object({
userType: yup.string().required(),
});
const form = useForm({
resolver: yupResolver(schema),
defaultValues: {
userType: "individual"
}
});
На этом этапе Yup Resolver обеспечивает базовую проверку без условной логики.
Основной инструмент построения динамической логики в Yup — метод
when.
const schema = yup.object({
userType: yup.string().required(),
firstName: yup.string().when("userType", {
is: "individual",
then: (schema) => schema.required("Обязательное поле"),
otherwise: (schema) => schema.notRequired()
}),
companyName: yup.string().when("userType", {
is: "company",
then: (schema) => schema.required(),
otherwise: (schema) => schema.notRequired()
})
});
Такой подход позволяет избегать создания нескольких отдельных схем, сохраняя единую точку валидации.
Важно понимать, что Yup Resolver не управляет UI. Он лишь валидирует данные. Поэтому скрытие полей должно сопровождаться очисткой состояния.
При переключении userType значения в скрытых секциях
остаются в форме:
userType = "individual"
company.companyName = "ACME"
Без очистки это значение продолжит участвовать в submit.
import { useEffect } from "react";
useEffect(() => {
if (form.watch("userType") === "individual") {
form.resetField("company");
} else {
form.resetField("individual");
}
}, [form.watch("userType")]);
Сложные формы часто содержат многоуровневые зависимости.
Пример:
const schema = yup.object({
userType: yup.string(),
company: yup.object({
type: yup.string().when("userType", {
is: "company",
then: (schema) => schema.required()
}),
vatNumber: yup.string().when("type", {
is: "legal",
then: (schema) => schema.required()
})
})
});
Особенность вложенных схем заключается в необходимости правильного указания контекста зависимости.
В Yup можно явно указывать зависимости через массив:
yup.string().when(["userType", "company.type"], {
is: (userType, companyType) =>
userType === "company" && companyType === "legal",
then: (schema) => schema.required()
});
Такой подход используется при сложных бизнес-правилах, где одно поле зависит сразу от нескольких параметров.
При работе с React Hook Form условные секции обычно строятся через
watch:
const userType = form.watch("userType");
{userType === "individual" && (
<div>
<input {...register("firstName")} />
<input {...register("lastName")} />
</div>
)}
{userType === "company" && (
<div>
<input {...register("company.companyName")} />
<input {...register("company.vatNumber")} />
</div>
)}
UI и схема остаются независимыми слоями:
При переключении секций важно не только скрывать поля, но и обеспечивать консистентность данных.
form.setValue("company", {
companyName: "",
vatNumber: ""
});
form.reset({
userType: "individual",
individual: {
firstName: "",
lastName: ""
},
company: {}
});
Часто секции включают динамические списки:
{
userType: "company",
employees: [
{ name: "", role: "" }
]
}
employees: yup.array().when("userType", {
is: "company",
then: (schema) =>
schema.of(
yup.object({
name: yup.string().required(),
role: yup.string().required()
})
),
otherwise: (schema) => schema.notRequired()
});
При скрытии секций важно учитывать nullable и
transform.
vatNumber: yup.string().when("userType", {
is: "individual",
then: (schema) => schema.strip(),
otherwise: (schema) => schema.required()
});
strip() полностью исключает поле из результата
валидации, предотвращая передачу лишних данных.
На практике часто применяется единая схема с переключающими условиями:
const schema = yup.object({
userType: yup.string().required(),
data: yup.mixed().when("userType", {
is: "individual",
then: () =>
yup.object({
firstName: yup.string().required(),
lastName: yup.string().required()
}),
otherwise: () =>
yup.object({
companyName: yup.string().required(),
vatNumber: yup.string().required()
})
})
});
Этот подход снижает количество конфликтов между полями разных секций.
Когда стандартного when недостаточно, используется
функция:
yup.string().when("userType", (userType, schema) => {
if (userType === "company") {
return schema.required();
}
return schema.notRequired();
});
Такой формат удобен при интеграции бизнес-логики, зависящей от внешних условий.
При большом количестве условных секций возникает проблема рассинхронизации:
Для стабилизации структуры используется комбинация:
watch для управления UIresetField для очисткиwhen для логики Yupstrip для исключения данныхЭта связка формирует устойчивую модель динамических форм без дублирования логики в разных слоях приложения.