Валидация на основе внешних данных

Валидация, основанная на внешних источниках данных, используется тогда, когда локальной проверки схемы недостаточно и требуется обращение к серверу, API или другой системе хранения. Такой подход особенно важен при проверке уникальности значений, актуальности справочных данных, прав доступа и бизнес-правил, которые невозможно выразить статическими ограничениями.

В связке с резолверами схем в формах, таких как YupResolver из экосистемы интеграции Yup и react-hook-form, появляется возможность комбинировать декларативные правила и асинхронные проверки, сохраняя единый подход к описанию валидации.


Природа внешней валидации

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

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

Такие операции обладают двумя ключевыми характеристиками:

Асинхронность Ответ приходит с задержкой, что требует поддержки Promise-механики.

Нестабильность результата Результат может меняться во времени даже для одинакового входа.


Механика Yup при асинхронной валидации

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

Базовая структура выглядит следующим образом:

import * as Yup from "yup";

const schema = Yup.object({
  email: Yup.string()
    .email("Некорректный формат email")
    .required("Обязательное поле")
    .test("check-email", "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;
    })
});

Асинхронный test возвращает true или false, либо выбрасывает исключение, которое трактуется как ошибка валидации.


Интеграция YupResolver

Роль резолвера заключается в преобразовании схемы Yup в формат, который понимает форма. В контексте react-hook-form это происходит через @hookform/resolvers/yup.

YupResolver выполняет следующие задачи:

  • запускает валидацию всей схемы;
  • обрабатывает синхронные и асинхронные проверки;
  • нормализует ошибки в структуру react-hook-form;
  • возвращает результат в виде { values, errors }.

Пример подключения:

import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as Yup from "yup";

const schema = Yup.object({
  username: Yup.string()
    .required()
    .test("check-username", "Имя занято", async (value) => {
      const res = await fetch(`/api/username/${value}`);
      const data = await res.json();
      return data.available;
    })
});

function Form() {
  const { register, handleSubmit, formState } = useForm({
    resolver: yupResolver(schema)
  });

  return null;
}

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

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

Последовательность выполнения

Все test-проверки внутри Yup выполняются последовательно. Это означает, что:

  • если базовая проверка (required, min, matches) не пройдена, асинхронный запрос не выполняется;
  • асинхронные проверки выполняются только при достижении их уровня в цепочке.

Это позволяет экономить сетевые ресурсы.


Конкурентные запросы

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

  • запрос A отправлен для значения user1;
  • запрос B отправлен для значения user12;
  • ответ A приходит позже B и перезаписывает результат.

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

Решения:

  • отмена запросов через AbortController;
  • игнорирование устаревших ответов;
  • debounce на уровне UI;
  • кэширование результатов.

Пример с AbortController

let controller;

Yup.string().test("check", "Ошибка", async (value) => {
  if (controller) controller.abort();

  controller = new AbortController();

  const res = await fetch(`/api/check?value=${value}`, {
    signal: controller.signal
  });

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

Разделение ответственности между схемой и API

При усложнении логики не рекомендуется перегружать Yup-схему прямыми HTTP-вызовами. Более устойчивый подход заключается в вынесении логики в сервисный слой.

async function checkEmailAvailability(email) {
  const res = await fetch(`/api/email/check`, {
    method: "POST",
    body: JSON.stringify({ email })
  });

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

И использование в схеме:

Yup.string().test("email-check", "Email занят", async (value) => {
  return await checkEmailAvailability(value);
});

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


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

При частых проверках одних и тех же значений имеет смысл использовать кэширование. Это снижает нагрузку на API и ускоряет реакцию интерфейса.

const cache = new Map();

async function cachedCheckUsername(username) {
  if (cache.has(username)) {
    return cache.get(username);
  }

  const res = await fetch(`/api/username/${username}`);
  const data = await res.json();

  cache.set(username, data.available);

  return data.available;
}

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

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

Основные стратегии:

Fallback-разрешение При ошибке сервиса считать поле валидным:

.test("check", async (value) => {
  try {
    return await apiCheck(value);
  } catch {
    return true;
  }
});

Жёсткая блокировка При ошибке считать поле невалидным:

.test("check", async (value) => {
  try {
    return await apiCheck(value);
  } catch {
    return false;
  }
});

Выбор стратегии определяется критичностью данных.


Оптимизация частоты внешних запросов

Внешняя валидация должна быть минимально навязчивой для API. Основные техники оптимизации:

  • debounce на уровне поля ввода;
  • запуск проверки только при blur, а не change;
  • группировка запросов;
  • условный запуск тестов внутри Yup.

Пример условного запуска:

.test("check", async (value) => {
  if (value.length < 3) return true;
  return await apiCheck(value);
});

Согласование YupResolver и состояния формы

react-hook-form выполняет валидацию через резолвер при определённых триггерах:

  • onChange
  • onBlur
  • onSubmit

При использовании YupResolver важно учитывать, что:

  • каждый триггер может инициировать асинхронные проверки;
  • состояние isValid становится нестабильным при частых изменениях;
  • formState.errors обновляется после завершения всех промисов.

Паттерн отложенной серверной проверки

Комбинация локальной и внешней валидации часто реализуется через двухэтапный подход:

  1. Локальная проверка схемы Yup;
  2. Серверная проверка только при прохождении первого этапа.
const schema = Yup.object({
  code: Yup.string()
    .required()
    .min(6)
    .test("verify-code", async (value) => {
      if (!value) return false;
      return await verifyPromoCode(value);
    })
});

Управление пользовательским опытом при внешней валидации

Асинхронная валидация влияет на поведение интерфейса:

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

Хотя сама схема Yup не управляет UI, YupResolver передаёт промежуточные состояния в форму, которые могут использоваться для отображения статуса проверки.


Типовые ошибки при использовании внешней валидации

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

Согласованность данных между клиентом и сервером

Внешняя валидация не должна рассматриваться как единственный источник истины. Даже при наличии Yup-схемы с асинхронными проверками сервер обязан повторно валидировать данные, поскольку клиентская логика может быть:

  • изменена;
  • обойдена;
  • частично выполнена из-за ошибок сети.

YupResolver в этом контексте выступает как инструмент предварительной проверки, оптимизирующий пользовательский ввод, но не заменяющий серверную валидацию.