Асинхронная валидация

Асинхронная валидация в контексте YupResolver приобретает ключевое значение при работе с формами, где часть правил зависит от удалённых источников данных: проверка уникальности логина, валидация email через сервер, сверка промокодов, динамические ограничения по бизнес-логике.

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

Асинхронная логика в этой архитектуре не выполняется напрямую в UI-компонентах. Она инкапсулируется внутри схемы Yup или внутри resolver-слоя, который трансформирует результат в формат ошибок React Hook Form.

Особенности асинхронного поведения в Yup

Асинхронные проверки в Yup реализуются через:

  • test с возвратом Promise
  • кастомные методы с async/await
  • внешние функции API-валидации

Базовая структура асинхронного теста:

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

Ключевое поведение:

  • Yup распознаёт Promise как сигнал асинхронной операции
  • результат интерпретируется как boolean или throw ValidationError
  • цепочка валидации приостанавливается до завершения Promise

Как YupResolver обрабатывает async schema

YupResolver выступает промежуточным слоем между формой и схемой.

При вызове resolver происходит следующая последовательность:

  1. Получение значений формы
  2. Вызов schema.validate(values, { abortEarly: false })
  3. Ожидание завершения всех async test внутри Yup
  4. Преобразование ошибок в формат React Hook Form
  5. Возврат результата { values, errors }

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

AbortEarly и влияние на async проверки

Параметр abortEarly: false критичен для асинхронной валидации.

yupResolver(schema, { abortEarly: false })

При true:

  • Yup прекращает проверку после первой ошибки
  • async тесты могут не выполниться полностью

При false:

  • все поля проходят валидацию
  • все async проверки запускаются параллельно
  • формируется полный набор ошибок

В контексте серверных проверок это предотвращает непредсказуемое состояние формы.

Параллельное выполнение async test

Асинхронные проверки внутри Yup не гарантируют строгую последовательность. При нескольких test:

username: yup.string()
  .test("check-username", async () => {...})
  .test("check-reserved", async () => {...})

Поведение:

  • оба теста могут выполняться независимо
  • порядок завершения не гарантирован
  • итоговый результат агрегируется resolver-ом

Это важно учитывать при зависимых проверках.

Проблема гонок запросов (race conditions)

При частом вводе значений возникает ситуация:

  • пользователь вводит a, затем ab, затем abc
  • каждый символ инициирует async validation
  • ответы могут приходить в произвольном порядке

Без защиты возможна ситуация, когда:

  • результат для a приходит позже и перезаписывает abc

Решение реализуется вне Yup, на уровне формы:

1. Дебаунс валидации

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

Дополнительно используется debounce на уровне input.

2. Контроль актуальности значения

.test("check-email", async function(value) {
  const current = this.parent.email;
  const res = await apiCheck(value);
  return value === current ? res.available : true;
});

3. AbortController (внешняя реализация)

let controller;

async function checkEmail(email) {
  if (controller) controller.abort();
  controller = new AbortController();

  return fetch(`/api/check?email=${email}`, {
    signal: controller.signal
  });
}

Асинхронная серверная валидация и кеширование

При интеграции с backend-валидацией важно минимизировать количество запросов.

Подходы:

Кеширование результатов

const cache = new Map();

const checkEmail = async (email) => {
  if (cache.has(email)) return cache.get(email);

  const result = await api(email);
  cache.set(email, result);

  return result;
};

Батчинг запросов

Вместо частых вызовов:

  • объединение проверок
  • отправка массива значений
  • распределение ответа

Ошибки асинхронной валидации

В Yup асинхронная ошибка может возникать в двух формах:

1. ValidationError

throw new yup.ValidationError("Ошибка сервера");

2. Boolean false

return false;

Разница:

  • false — простое нарушение правила
  • ValidationError — структурированная ошибка с path и type

React Hook Form использует это для привязки ошибки к конкретному полю.

Влияние режима валидации React Hook Form

Асинхронное поведение сильно зависит от режима:

  • onSubmit — async проверки выполняются только при сабмите
  • onBlur — при потере фокуса
  • onChange — при каждом изменении

При onChange нагрузка на async слой максимальна.

Композиция нескольких async правил

Типичный сценарий:

password: yup.string()
  .required()
  .test("strength", async (value) => checkStrength(value))
  .test("breached", async (value) => checkBreachDatabase(value))

Особенности:

  • проверки независимы
  • результат агрегируется
  • первая неудача не всегда останавливает остальные async операции

Зависимые поля и async логика

Асинхронные проверки часто зависят от других полей:

.test("check-zip-city", async function(value) {
  const city = this.parent.city;
  return apiValidateCityZip(city, value);
});

Здесь критично использовать this.parent, поскольку resolver передаёт актуальное состояние формы.

Оптимизация асинхронной нагрузки

При больших формах с множеством async правил возникает проблема деградации производительности.

Применяются стратегии:

1. Lazy validation

Выполнение async проверки только при определённых условиях:

.test("check-if-needed", async function(value) {
  if (!this.options.context.enableCheck) return true;
  return apiCheck(value);
});

2. Условная схема

when("role", {
  is: "admin",
  then: schema.test("admin-check", async () => {...})
});

3. Разделение синхронной и асинхронной логики

Сначала выполняется быстрый sync-layer, затем async-layer.

Поведение при ошибках сети

Асинхронная валидация должна учитывать:

  • таймауты
  • недоступность API
  • частичные ответы

Типичная стратегия:

.test("network-safe", async (value) => {
  try {
    return await apiCheck(value);
  } catch (e) {
    return true;
  }
});

В этом случае ошибка сети не блокирует ввод пользователя, но может логироваться отдельно.

Связь async validation и re-render модели

React Hook Form не вызывает лишние рендеры при каждом async шаге, но:

  • состояние formState.errors обновляется после завершения Promise
  • isValidating может использоваться для индикаторов загрузки
  • isValid пересчитывается после каждого resolver-цикла

Асинхронная валидация становится частью реактивного потока состояния формы, а не внешним эффектом.