Постепенная миграция больших форм

При работе с крупными формами в интерфейсах часто возникает необходимость перехода от разрозненной ручной валидации к централизованной схеме на основе Yup и интеграции через YupResolver в связке с React Hook Form. Полная замена подхода «за один шаг» в больших кодовых базах приводит к регрессиям, сложностям тестирования и непредсказуемым побочным эффектам. Поэтому используется стратегия постепенной миграции, при которой форма переводится на схему частями, сохраняя работоспособность на каждом этапе.


Базовая модель интеграции YupResolver

YupResolver служит мостом между схемой 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({
  firstName: yup.string().required(),
  age: yup.number().min(18),
});

const form = useForm({
  resolver: yupResolver(schema),
});

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

  • локальные валидаторы на уровне компонентов
  • условную валидацию
  • динамические поля
  • бизнес-правила, распределённые по UI

Стратегия частичного подключения схемы

Основной принцип миграции заключается в том, чтобы YupResolver применялся не ко всей форме, а к её изолированным частям.

Разделение формы на домены

Большие формы логически делятся на независимые блоки:

  • данные пользователя
  • адресная информация
  • платежные данные
  • настройки уведомлений

Каждый блок выделяется в отдельную Yup-схему:

const userSchema = yup.object({
  firstName: yup.string().required(),
  lastName: yup.string().required(),
});

const addressSchema = yup.object({
  city: yup.string().required(),
  zip: yup.string().matches(/^\d{6}$/),
});

На этапе миграции используется объединение:

const fullSchema = yup.object({
  ...userSchema.fields,
  ...addressSchema.fields,
});

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


Инкрементальная замена валидаторов

Во многих проектах форма уже содержит ручные проверки:

if (!values.email.includes("@")) {
  setError("email", "Invalid email");
}

Переход осуществляется поэтапно:

1. Сохранение старой логики

Старая проверка остаётся активной.

2. Добавление Yup-правила параллельно

email: yup.string().email("Invalid email"),

3. Сравнение результатов

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

4. Отключение ручной проверки

После стабилизации поведения старая логика удаляется.


Использование режимов частичной валидации

React Hook Form позволяет запускать валидацию по событиям:

  • onChange
  • onBlur
  • onSubmit

При миграции больших форм часто используется гибридный режим:

useForm({
  resolver: yupResolver(schema),
  mode: "onBlur",
  reValidateMode: "onChange",
});

Такой режим снижает нагрузку на частично мигрированные формы, где не все поля ещё описаны в Yup.


Частичное описание схемы (partial schema migration)

Yup позволяет использовать необязательные поля для временного сосуществования старой и новой логики.

const schema = yup.object({
  firstName: yup.string().required(),
  lastName: yup.string().notRequired(),
  legacyField: yup.mixed().notRequired(),
});

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


Динамические поля и ленивые схемы

Большие формы часто содержат динамически добавляемые блоки. В этом случае используется lazy:

const dynamicSchema = yup.lazy((value) => {
  if (value?.type === "company") {
    return yup.object({
      companyName: yup.string().required(),
      vat: yup.string().required(),
    });
  }

  return yup.object({
    personalId: yup.string().required(),
  });
});

При миграции важно не переносить всю динамику сразу, а вводить её по сценариям использования.


Комбинирование схем при частичной миграции

При поэтапном переходе часто возникает ситуация, когда часть формы использует Yup, а часть — legacy-валидацию. Тогда используется составной резолвер:

const resolver = async (values, context, options) => {
  const yupResult = await yupResolver(partialSchema)(values, context, options);
  const legacyErrors = legacyValidate(values);

  return {
    values,
    errors: {
      ...yupResult.errors,
      ...legacyErrors,
    },
  };
};

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


Миграция по полям (field-level migration)

Самый управляемый способ — перенос логики по одному полю.

Шаг 1: выделение поля

const schema = yup.object({
  email: yup.string().email().required(),
});

Шаг 2: подключение только одного поля к схеме

Остальные поля остаются вне YupResolver и валидируются вручную.

Шаг 3: расширение схемы

Постепенно добавляются новые поля:

const schema = yup.object({
  email: yup.string().email().required(),
  phone: yup.string().matches(/^\+?\d+$/),
});

Такой подход минимизирует риск регрессий.


Управление ошибками при смешанной валидации

При частичной миграции важно унифицировать формат ошибок.

Yup возвращает структуру:

{
  path: "email",
  message: "Invalid email"
}

Legacy-валидация часто возвращает:

{
  email: "Invalid email"
}

Для объединения используется адаптер:

const normalizeErrors = (errors) => {
  return Object.entries(errors).reduce((acc, [key, value]) => {
    acc[key] = {
      type: "manual",
      message: value.message || value,
    };
    return acc;
  }, {});
};

Работа с зависимыми полями

Большие формы часто содержат поля с зависимостями:

  • пароль / подтверждение пароля
  • страна / регион
  • тип аккаунта / набор обязательных полей

В Yup это выражается через when:

confirmPassword: yup
  .string()
  .oneOf([yup.ref("password")], "Passwords must match"),

При миграции такие зависимости переносятся в последнюю очередь, поскольку они чаще всего связаны с несколькими частями UI одновременно.


Оптимизация производительности при миграции

Переход на YupResolver может увеличить стоимость валидации, если схема становится слишком большой.

Применяются следующие техники:

1. Разделение схем

const resolver = yupResolver(partialSchemaA);

и подключение через композицию только активных блоков формы.

2. Условное подключение

Схема активируется только при необходимости:

const schema = useMemo(() => {
  if (step === 2) return step2Schema;
  return step1Schema;
}, [step]);

3. Минимизация пересборки схемы

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


Тестирование промежуточных состояний формы

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

Проверяются:

  • совпадение ошибок между legacy и Yup
  • корректность частично валидируемых данных
  • отсутствие регрессий при отключении старых валидаторов

Пример теста:

expect(yupResult.errors.email).toEqual(legacyResult.email);

Типизация при поэтапной миграции

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

Используется стратегия постепенного расширения типов:

type FormValues = {
  email: string;
  // временно необязательные поля
  phone?: string;
};

После полной миграции типы синхронизируются с Yup:

type FormValues = yup.InferType<typeof schema>;

Управление рисками при частичной миграции

Основные риски:

  • расхождение логики валидации
  • дублирование проверок
  • несогласованность ошибок UI
  • деградация производительности

Для контроля используются:

  • feature flags для включения Yup по частям
  • параллельный режим валидации
  • логирование различий результатов

Интеграция с многошаговыми формами

В wizard-формах миграция выполняется по шагам:

const schemas = [
  step1Schema,
  step2Schema,
  step3Schema,
];

Каждый шаг получает свой resolver:

resolver: yupResolver(schemas[currentStep])

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


Синхронизация состояния формы и схемы

При постепенном переходе важно избегать ситуации, когда UI уже изменён, а схема ещё нет.

Используется единая точка управления:

  • схема — источник истины для новых полей
  • legacy-валидация — временный слой совместимости

Постепенно legacy-слой сокращается до полного исчезновения, а YupResolver становится единственным механизмом валидации.