Многошаговые формы (wizard forms) в веб-приложениях представляют собой последовательность логически связанных шагов, где пользователь постепенно вводит данные, разбитые на отдельные экраны. В таких сценариях ключевую роль играет управление состоянием, валидацией и синхронизацией данных между шагами.
В связке с React Hook Form и Yup используется YupResolver — адаптер, который позволяет подключать Yup-схемы к системе валидации React Hook Form.
Основная задача YupResolver — преобразовать декларативную схему Yup в формат, понятный механизму React Hook Form, обеспечивая:
В wizard-формах это особенно важно, поскольку каждый шаг может иметь собственную под-схему валидации, а общая форма остаётся единым объектом состояния.
Wizard-форма обычно моделируется как набор логических этапов:
Каждый шаг соответствует отдельному набору полей и собственной логике проверки. При этом данные не теряются при переходе между шагами, а аккумулируются в общем состоянии формы.
Ключевая особенность: валидация должна быть локальной для текущего шага, но итоговая отправка — глобальной.
Подключение 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.
В многошаговых формах редко требуется проверка всех полей сразу. Вместо этого используется частичная валидация:
await trigger(["firstName", "lastName"]);
Это позволяет:
YupResolver при этом валидирует только переданные поля, опираясь на текущую схему.
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;
}, {});
};
Это предотвращает отображение ошибок будущих шагов, которые пользователь ещё не заполнял.
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;const form = useForm({
resolver: yupResolver(getSchema(step)),
shouldUnregister: false,
});
При использовании 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.
YupResolver пересоздаётся при изменении схемы. Это критический момент:
Чтобы избежать проблем, схема часто мемоизируется:
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-формах часто возникают типовые проблемы:
trigger.Каждая из этих ошибок приводит к деградации UX и усложнению поддержки.
При росте количества шагов система валидации должна оставаться предсказуемой. На практике применяются следующие принципы:
Такой подход сохраняет стабильность поведения формы даже при значительном усложнении сценариев ввода данных.