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

Асинхронная валидация в Yup опирается на возможность использовать функции, возвращающие Promise, внутри пользовательских правил проверки. Это позволяет интегрировать проверки, требующие обращения к внешним источникам данных: API, базе данных, сервисам авторизации и любым асинхронным вычислениям.

Основной механизм асинхронной валидации в Yup реализуется через метод .test. Если функция теста возвращает Promise, Yup автоматически переходит в режим ожидания результата.

import * as Yup from 'yup';

const schema = Yup.object({
  email: Yup.string()
    .email()
    .required()
    .test(
      'check-email-unique',
      'Такой email уже используется',
      async (value) => {
        if (!value) return true;

        const response = await fetch(`/api/check-email?email=${value}`);
        const data = await response.json();

        return data.available === true;
      }
    ),
});

Ключевая особенность заключается в том, что возвращаемое значение может быть:

  • true — проверка пройдена
  • false — ошибка валидации
  • Promise<boolean> — асинхронная проверка

Контекст выполнения теста

Функция .test получает доступ к контексту, который позволяет работать с текущим значением объекта, путём к полю и дополнительными параметрами валидации.

Yup.string().test('check', function (value) {
  const { path, createError } = this;

  return new Promise((resolve) => {
    setTimeout(() => {
      if (value !== 'admin') {
        resolve(true);
      } else {
        resolve(
          createError({
            path,
            message: 'Значение admin запрещено',
          })
        );
      }
    }, 500);
  });
});

Использование this.createError позволяет формировать кастомные ошибки с точной привязкой к полю схемы.

Особенности выполнения асинхронных проверок

Асинхронные тесты в Yup выполняются последовательно в рамках одного поля, но схема в целом может валидировать несколько полей параллельно. Это создаёт важные поведенческие особенности:

  • каждое поле может ожидать собственный Promise
  • общая валидация объекта возвращает Promise
  • результат агрегируется после завершения всех проверок
await schema.validate(data, { abortEarly: false });

Опция abortEarly: false влияет только на синхронные ошибки внутри одного поля, но не отменяет выполнение асинхронных проверок.

Использование .isValid и .validate

Метод .validate возвращает либо валидированный объект, либо выбрасывает ошибку, содержащую структуру нарушений.

try {
  await schema.validate(data);
} catch (err) {
  console.log(err.errors);
}

Метод .isValid используется для получения булевого результата без выброса исключений:

const isValid = await schema.isValid(data);

В обоих случаях асинхронные тесты выполняются полностью, поскольку результат зависит от завершения всех Promise.

Асинхронные проверки уникальности

Типичный сценарий — проверка уникальности логина, email или другого идентификатора через сервер.

const schema = Yup.object({
  username: Yup.string()
    .required()
    .test('unique-username', async function (value) {
      const res = await fetch(`/api/users/check?username=${value}`);
      const { exists } = await res.json();

      if (exists) {
        return this.createError({
          message: 'Пользователь уже существует',
        });
      }

      return true;
    }),
});

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

Потенциальные проблемы гонок запросов

При частом изменении значения поля (например, ввод в input) асинхронные проверки могут создавать эффект гонки запросов. Старые запросы могут завершаться позже новых и перезаписывать результат.

Yup сам по себе не отменяет предыдущие Promise. Поэтому управление этой ситуацией обычно выносится на уровень приложения:

  • использование debounce перед запуском валидации
  • отмена запросов через AbortController
  • хранение актуального значения поля
let controller;

const schema = Yup.string().test('check', async (value) => {
  if (controller) controller.abort();
  controller = new AbortController();

  const res = await fetch('/api/check', {
    signal: controller.signal,
  });

  return res.ok;
});

Условная асинхронная валидация через .when

Метод .when позволяет включать асинхронные проверки только при выполнении определённых условий.

const schema = Yup.object({
  password: Yup.string().when('isAdmin', {
    is: true,
    then: (s) =>
      s.test('check-admin-password', async (value) => {
        const res = await fetch('/api/admin/check-password', {
          method: 'POST',
          body: JSON.stringify({ value }),
        });

        const data = await res.json();
        return data.valid;
      }),
  }),
});

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

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

Валидация массивов с асинхронными тестами выполняется для каждого элемента независимо.

const schema = Yup.object({
  emails: Yup.array().of(
    Yup.string().test('check-email', async (value) => {
      const res = await fetch(`/api/check-email?email=${value}`);
      const data = await res.json();
      return data.available;
    })
  ),
});

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

Производительность асинхронных схем

Асинхронные валидаторы напрямую влияют на время выполнения схемы. При большом количестве полей с сетевыми запросами возникают характерные узкие места:

  • задержка из-за последовательных await внутри тестов
  • нагрузка на API при массовой валидации
  • блокировка UI при отсутствии оптимизации

Оптимизация обычно включает:

  • кеширование результатов проверок
  • батчинг запросов
  • сокращение количества асинхронных тестов
  • перенос части логики в серверный слой

Возврат ошибок из асинхронного теста

Ошибка может быть возвращена как false, либо как объект ошибки через createError.

return this.createError({
  message: 'Ошибка проверки на сервере',
});

Также допустим вариант выбрасывания исключения внутри Promise, однако в Yup предпочтительно использовать явное возвращаемое значение.

Взаимодействие с зависимыми полями

Асинхронные проверки часто зависят от других значений схемы через this.parent.

Yup.string().test('check-match', async function (value) {
  const password = this.parent.password;

  const res = await fetch('/api/check-password-match', {
    method: 'POST',
    body: JSON.stringify({ password, confirmation: value }),
  });

  const data = await res.json();
  return data.match;
});

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

Поведение при отмене валидации

Yup не предоставляет встроенного механизма отмены уже запущенных асинхронных тестов. Если схема запускается повторно, старые Promise продолжают выполняться в фоне. Это важно учитывать при интеграции с UI-библиотеками форм, где валидация может вызываться на каждый ввод символа.

Стратегии управления включают:

  • контроль частоты вызова validate
  • использование debounce на уровне формы
  • блокировку устаревших результатов через идентификаторы запросов
let validationId = 0;

Yup.string().test('check', async (value) => {
  const currentId = ++validationId;

  const res = await fetch('/api/check');
  const result = await res.json();

  return currentId === validationId && result.ok;
});

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

Асинхронные кастомные методы через addMethod

Расширение Yup позволяет создавать переиспользуемые асинхронные правила.

Yup.addMethod(Yup.string, 'unique', function (message) {
  return this.test('unique', message, async function (value) {
    const res = await fetch(`/api/unique?value=${value}`);
    const data = await res.json();
    return data.unique;
  });
});

const schema = Yup.object({
  login: Yup.string().unique('Логин занят'),
});

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

Итоговые особенности поведения

Асинхронная валидация в Yup строится вокруг Promise-модели и интегрируется в синхронную структуру схемы без разделения API. Это создаёт единый механизм обработки правил, где:

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