Wizard формы с несколькими шагами

Многошаговые формы (wizard forms) в веб-приложениях представляют собой последовательность логически связанных шагов, где пользователь постепенно вводит данные, разбитые на отдельные экраны. В таких сценариях ключевую роль играет управление состоянием, валидацией и синхронизацией данных между шагами.

В связке с React Hook Form и Yup используется YupResolver — адаптер, который позволяет подключать Yup-схемы к системе валидации React Hook Form.

Основная задача YupResolver — преобразовать декларативную схему Yup в формат, понятный механизму React Hook Form, обеспечивая:

  • синхронную и асинхронную валидацию;
  • структурированное отображение ошибок;
  • интеграцию схемы в жизненный цикл формы без ручной обработки значений.

В wizard-формах это особенно важно, поскольку каждый шаг может иметь собственную под-схему валидации, а общая форма остаётся единым объектом состояния.


Принцип разбиения формы на шаги

Wizard-форма обычно моделируется как набор логических этапов:

  1. Личные данные
  2. Контактная информация
  3. Адрес
  4. Подтверждение

Каждый шаг соответствует отдельному набору полей и собственной логике проверки. При этом данные не теряются при переходе между шагами, а аккумулируются в общем состоянии формы.

Ключевая особенность: валидация должна быть локальной для текущего шага, но итоговая отправка — глобальной.


Базовая интеграция YupResolver

Подключение YupResolver выполняется через useForm:

import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";

const schema = yup.object({
  firstName: yup.string().required(),
  lastName: yup.string().required(),
});
const form = useForm({
  resolver: yupResolver(schema),
  mode: "onChange",
});

В классическом сценарии схема одна. Однако в wizard-формах требуется динамическая смена схемы в зависимости от текущего шага.


Динамическая схема валидации по шагам

Для многошаговой формы часто используется подход с переключением схем:

const stepSchemas = [
  yup.object({
    firstName: yup.string().required("Имя обязательно"),
    lastName: yup.string().required("Фамилия обязательна"),
  }),

  yup.object({
    email: yup.string().email().required("Email обязателен"),
    phone: yup.string().required("Телефон обязателен"),
  }),

  yup.object({
    city: yup.string().required(),
    address: yup.string().required(),
  }),
];

Далее текущая схема выбирается динамически:

const currentSchema = stepSchemas[currentStep];

const form = useForm({
  resolver: yupResolver(currentSchema),
  mode: "onBlur",
});

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


Сохранение состояния между шагами

Wizard-форма требует сохранения данных при переключении шагов. React Hook Form по умолчанию сохраняет значения, если компонент не размонтируется.

Пример управления шагами:

const [step, setStep] = useState(0);

const nextStep = async () => {
  const valid = await trigger(); // проверка текущей схемы
  if (valid) setStep((s) => s + 1);
};

Функция trigger() активирует валидацию текущего шага через YupResolver.


Частичная валидация и trigger

В многошаговых формах редко требуется проверка всех полей сразу. Вместо этого используется частичная валидация:

await trigger(["firstName", "lastName"]);

Это позволяет:

  • проверять только активные поля;
  • избегать ошибок от незаполненных будущих шагов;
  • ускорять UX переходов.

YupResolver при этом валидирует только переданные поля, опираясь на текущую схему.


Условная логика внутри Yup-схем

Wizard-формы часто требуют динамической логики: поля появляются или исчезают в зависимости от предыдущих шагов.

Yup поддерживает условную валидацию через when:

const schema = yup.object({
  hasCompany: yup.boolean(),

  companyName: yup.string().when("hasCompany", {
    is: true,
    then: (schema) => schema.required(),
    otherwise: (schema) => schema.notRequired(),
  }),
});

В wizard-форме это используется для адаптации шагов без изменения структуры формы.


Разделение схем по контексту шагов

Более масштабируемый подход — изоляция схем в отдельные модули:

/validation
  step1Schema.js
  step2Schema.js
  step3Schema.js

И объединение через индекс шагов:

const getSchema = (step) => {
  switch (step) {
    case 0:
      return step1Schema;
    case 1:
      return step2Schema;
    case 2:
      return step3Schema;
    default:
      return yup.object();
  }
};

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


Управление ошибками между шагами

React Hook Form хранит ошибки в объекте formState.errors. В wizard-формах важно отображать только ошибки текущего шага:

const getStepErrors = (errors, stepFields) => {
  return Object.keys(errors)
    .filter((key) => stepFields.includes(key))
    .reduce((acc, key) => {
      acc[key] = errors[key];
      return acc;
    }, {});
};

Это предотвращает отображение ошибок будущих шагов, которые пользователь ещё не заполнял.


Асинхронная валидация в wizard-формах

YupResolver поддерживает асинхронные проверки, например проверку уникальности email:

const schema = yup.object({
  email: yup
    .string()
    .email()
    .required()
    .test("unique", "Email занят", async (value) => {
      const res = await fetch(`/api/check-email?email=${value}`);
      const data = await res.json();
      return data.available;
    }),
});

В wizard-формах такие проверки обычно выносятся на последний шаг, чтобы минимизировать количество запросов.


Оптимизация переходов между шагами

Основная проблема wizard-форм — избыточные ререндеры и повторная валидация.

Практика оптимизации:

  • использовать mode: "onBlur" вместо onChange;
  • разделять шаги на компоненты;
  • использовать shouldUnregister: false;
  • кэшировать схемы, чтобы не пересоздавать Yup-объекты.
const form = useForm({
  resolver: yupResolver(getSchema(step)),
  shouldUnregister: false,
});

Типизация в TypeScript

При использовании TypeScript схема Yup может быть связана с типами формы:

import { InferType } from "yup";

const schema = yup.object({
  name: yup.string().required(),
  age: yup.number().required(),
});

type FormData = InferType<typeof schema>;

В wizard-формах тип обычно объединяет все шаги:

type WizardData = {
  firstName: string;
  lastName: string;
  email: string;
  address: string;
};

Это обеспечивает единое состояние, несмотря на разбиение UI.


Поведение resolver в динамических формах

YupResolver пересоздаётся при изменении схемы. Это критический момент:

  • изменение step → новая схема → новая функция валидации;
  • возможен сброс ошибок при переключении;
  • требуется аккуратное управление состоянием.

Чтобы избежать проблем, схема часто мемоизируется:

const resolver = useMemo(
  () => yupResolver(getSchema(step)),
  [step]
);

Обработка финальной отправки

На последнем шаге выполняется полная валидация всей агрегированной модели:

const onSub mit = async (data) => {
  const fullSchema = stepSchemas.reduce(
    (acc, schema) => acc.concat(schema)
  );

  await fullSchema.validate(data, { abortEarly: false });

  await fetch("/api/submit", {
    method: "POST",
    body: JSON.stringify(data),
  });
};

В этом сценарии YupResolver уже не используется напрямую, поскольку проверка становится глобальной.


Распространённые архитектурные ошибки

В сложных wizard-формах часто возникают типовые проблемы:

  • использование одной монолитной схемы для всех шагов;
  • отсутствие изоляции полей между шагами;
  • повторная инициализация формы при каждом шаге;
  • смешивание UI-логики и валидации;
  • отсутствие частичной валидации через trigger.

Каждая из этих ошибок приводит к деградации UX и усложнению поддержки.


Масштабирование сложных wizard-форм

При росте количества шагов система валидации должна оставаться предсказуемой. На практике применяются следующие принципы:

  • схемы описывают только структуру данных, без UI-логики;
  • шаги определяют только набор полей;
  • YupResolver остаётся единственным слоем интеграции валидации;
  • переходы между шагами не должны влиять на внутреннее состояние формы.

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