Совместимость с legacy кодом

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

Особенности legacy-архитектур форм

Наследуемые системы обычно обладают следующими характеристиками:

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

В таких условиях внедрение YupResolver требует промежуточного слоя совместимости.


Роль YupResolver как адаптера между схемой и legacy-логикой

YupResolver в экосистеме React Hook Form выполняет функцию транслятора схемы Yup в формат, понятный системе управления формами. Однако в контексте legacy-кода он становится дополнительным адаптером между старой и новой моделью валидации.

Ключевая идея интеграции:

Yup schema → YupResolver → RHF → legacy handlers

При этом legacy-слой не обязан знать о существовании Yup или resolver-логики.


Инкапсуляция старой валидации через обёртки

Один из наиболее безопасных способов внедрения заключается в создании адаптера вокруг существующих функций проверки.

Пример legacy-валидатора

function legacyValidate(values) {
  const errors = {};

  if (!values.email || values.email.indexOf('@') === -1) {
    errors.email = "Некорректный email";
  }

  if (values.password && values.password.length < 6) {
    errors.password = "Слишком короткий пароль";
  }

  return errors;
}

Такая функция возвращает объект ошибок, но не соответствует формату Yup.

Оборачивание в совместимый слой

import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';

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

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

  const legacyErrors = legacyValidate(values);

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

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


Стратегия поэтапной миграции

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

Этап 1: параллельная валидация

На первом этапе обе системы работают одновременно:

  • Yup отвечает за базовую структуру данных;
  • legacy-валидаторы продолжают выполнять бизнес-проверки.
const resolver = async (values, context, options) => {
  const yupResult = await yupResolver(schema)(values, context, options);
  const legacyErrors = legacyValidate(values);

  const mergedErrors = {
    ...yupResult.errors,
    ...legacyErrors
  };

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

Главная цель — не допустить деградации функционала.


Этап 2: миграция по полям

Legacy-валидация постепенно отключается на отдельных полях.

const legacyValidate = (values) => {
  const errors = {};

  // email уже мигрирован в Yup
  if (!values.username) {
    errors.username = "Обязательное поле";
  }

  return errors;
};

Этап 3: полный отказ от legacy слоя

После завершения миграции resolver становится чистым:

const resolver = yupResolver(schema);

Совместимость с callback-ориентированными системами

Многие старые формы используют событийную модель:

form.onSub mit = function(values, callback) {
  const errors = legacyValidate(values);

  if (Object.keys(errors).length > 0) {
    callback(errors);
    return;
  }

  api.save(values, callback);
};

Интеграция через промежуточный bridge

const bridgeSubmit = async (values) => {
  const result = await resolver(values);

  if (Object.keys(result.errors).length > 0) {
    return result.errors;
  }

  return new Promise((resolve, reject) => {
    api.save(result.values, (err, response) => {
      if (err) reject(err);
      else resolve(response);
    });
  });
};

Таким образом, YupResolver становится первым этапом пайплайна.


Нормализация данных между слоями

Legacy-системы часто используют нестандартизированные структуры данных:

  • строки вместо чисел;
  • null вместо undefined;
  • вложенные объекты с разной глубиной;
  • плоские структуры вместо nested values.

Проблема несовпадения схем

Yup ожидает строгую структуру, тогда как legacy-код допускает вариативность.

Решение через pre-processing слой

function normalize(values) {
  return {
    ...values,
    age: Number(values.age || 0),
    profile: values.profile || {},
    email: (values.email || '').trim()
  };
}

Подключение к resolver

const resolver = async (values, context, options) => {
  const normalized = normalize(values);

  const result = await yupResolver(schema)(normalized, context, options);

  return result;
};

Интеграция с legacy UI-ошибками

В старых системах ошибки часто хранятся в DOM или отдельных объектах состояния:

formState.errors.emailMessage = "Ошибка";

Маппинг ошибок Yup в legacy-формат

function mapErrorsToLegacyFormat(errors) {
  const legacy = {};

  Object.keys(errors).forEach(key => {
    legacy[key + "Message"] = errors[key]?.message;
  });

  return legacy;
}

Обратная совместимость

const result = await resolver(values);

const legacyErrors = mapErrorsToLegacyFormat(result.errors);

legacyForm.setErrors(legacyErrors);

Работа с кастомными валидаторами

Legacy-код часто содержит доменные проверки, не выражаемые через Yup.

Пример:

function checkBusinessRules(values) {
  if (values.country === "restricted" && values.amount > 1000) {
    return {
      amount: "Превышен лимит для выбранной страны"
    };
  }

  return {};
}

Подключение через трансформирующий resolver

const resolver = async (values) => {
  const yupResult = await yupResolver(schema)(values);

  const businessErrors = checkBusinessRules(values);

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

Совместимость с Redux-based формами

В старых архитектурах формы часто управляются через Redux:

  • state хранится глобально;
  • валидация запускается через actions;
  • ошибки диспатчатся отдельно.

Bridge между resolver и Redux

const validateForm = (values) => async (dispatch) => {
  const result = await resolver(values);

  dispatch({
    type: "FORM_ERRORS_UPDATED",
    payload: result.errors
  });

  if (Object.keys(result.errors).length === 0) {
    dispatch({ type: "FORM_VALID" });
  }
};

Асинхронные legacy-валидаторы

Некоторые системы используют серверную проверку в реальном времени:

function checkEmailExists(email, callback) {
  api.check(email, callback);
}

Интеграция с YupResolver через async refinement

const schema = yup.object({
  email: yup.string().email().test(
    "exists",
    "Email уже используется",
    async (value) => {
      const res = await api.check(value);
      return !res.exists;
    }
  )
});

В legacy-контексте это позволяет заменить callback-модель на promise-based подход без изменения интерфейса формы.


Частичная интеграция в многослойных формах

В сложных системах форма может состоять из нескольких независимых модулей:

  • billing form;
  • profile form;
  • preferences form.

Изоляция resolver на уровне модулей

const billingResolver = yupResolver(billingSchema);
const profileResolver = yupResolver(profileSchema);

Объединение результатов

const combinedResolver = async (values) => {
  const billing = await billingResolver(values.billing);
  const profile = await profileResolver(values.profile);

  return {
    values: {
      billing: billing.values,
      profile: profile.values
    },
    errors: {
      billing: billing.errors,
      profile: profile.errors
    }
  };
};

Обработка несовместимых ошибок

Legacy-системы часто используют разные уровни severity:

  • warning;
  • error;
  • critical.

Yup работает только с ошибками.

Расширение структуры

function extendErrors(errors) {
  const extended = {};

  Object.keys(errors).forEach(key => {
    extended[key] = {
      message: errors[key].message,
      severity: "error"
    };
  });

  return extended;
}

Сосуществование двух моделей валидации

В процессе миграции важно сохранять обе модели:

  • Yup как источник истины для структуры данных;
  • legacy как источник доменных исключений.

Такой подход позволяет:

  • минимизировать риски регрессии;
  • постепенно сокращать legacy-код;
  • не нарушать существующие UI контракты;
  • сохранять предсказуемость поведения формы.

Контроль точек интеграции

Критические места внедрения YupResolver в legacy-систему:

  • submit pipeline;
  • field-level validation triggers;
  • async blur handlers;
  • global form state store;
  • server sync layer.

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


Стабилизация поведения после интеграции

После подключения YupResolver в legacy-окружение часто возникают различия в:

  • времени появления ошибок;
  • приоритете сообщений;
  • порядке валидации;
  • обработке пустых значений.

Стабилизация достигается через фиксированные правила мержа:

  • Yup ошибки первичны;
  • legacy ошибки дополняют, но не заменяют;
  • серверные ошибки имеют высший приоритет;
  • UI отображает единый нормализованный формат.