Формы с условными разделами

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

В экосистеме React Hook Form связующим звеном между схемой валидации и формой выступает Yup Resolver, обеспечивающий синхронизацию данных формы с правилами, заданными в Yup-схеме.

Базовая архитектура условных форм

Условная форма строится вокруг нескольких ключевых принципов:

  • наличие управляющего поля (switcher, select, radio)
  • динамическое отображение секций
  • условная валидация схемы
  • синхронизация состояния с удалением неактуальных значений

Типичная структура данных:

{
  userType: "individual",
  individual: {
    firstName: "",
    lastName: ""
  },
  company: {
    companyName: "",
    vatNumber: ""
  }
}

В зависимости от userType активируется либо блок individual, либо company.

Подключение Yup Resolver к форме

Интеграция осуществляется через 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 обеспечивает базовую проверку без условной логики.


Условная валидация через when

Основной инструмент построения динамической логики в 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.

Решение через resetField

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()
});

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


Интеграция с динамическими UI-секциями

При работе с 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 и схема остаются независимыми слоями:

  • UI управляет отображением
  • Yup Resolver управляет правилами
  • React Hook Form управляет состоянием

Контроль очистки данных при смене секции

При переключении секций важно не только скрывать поля, но и обеспечивать консистентность данных.

Полная очистка через setValue

form.setValue("company", {
  companyName: "",
  vatNumber: ""
});

Или глубокий reset

form.reset({
  userType: "individual",
  individual: {
    firstName: "",
    lastName: ""
  },
  company: {}
});

Условные массивы полей

Часто секции включают динамические списки:

{
  userType: "company",
  employees: [
    { name: "", role: "" }
  ]
}

Yup для массивов с условиями

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 значений

При скрытии секций важно учитывать 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();
});

Такой формат удобен при интеграции бизнес-логики, зависящей от внешних условий.


Согласование схемы и UI при высокой динамике

При большом количестве условных секций возникает проблема рассинхронизации:

  • UI скрывает поле
  • схема продолжает его валидировать
  • данные остаются в state

Для стабилизации структуры используется комбинация:

  • watch для управления UI
  • resetField для очистки
  • when для логики Yup
  • strip для исключения данных

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