Асинхронная валидация в связке Yup и resolver-адаптеров (чаще всего используемых в формах React Hook Form) представляет собой один из наиболее сложных для отладки сценариев. Основная трудность заключается в том, что поток выполнения становится нелинейным: результат валидации зависит от промисов, внешних источников данных и порядка их разрешения.
В типичном случае YupResolver преобразует схему Yup в
формат, понятный форме, и возвращает либо синхронный результат, либо
Promise, содержащий ошибки. Именно асинхронная ветка
поведения становится источником скрытых багов: гонок запросов,
устаревших ошибок, несогласованного состояния формы.
При использовании асинхронных методов Yup, например test
с промисом или when с динамическими зависимостями, схема
перестаёт быть чисто декларативной.
Типичный поток выглядит следующим образом:
YupResolver запускает schema.validateerrorsКлючевая проблема возникает между шагами 3 и 6: состояние формы может измениться несколько раз, но предыдущие промисы продолжают выполняться.
Наиболее частая проблема — race condition между валидациями одного и того же поля.
Пример сценария:
aabЕсли результат не синхронизирован с актуальным значением, возможна ситуация:
YupResolver не отменяет предыдущие промисы. Каждая валидация — независимая операция.
Асинхронная валидация работает с “снимком” значений. Если внутри
test используется внешний источник или переменная,
изменяющаяся во времени, возможна рассинхронизация.
Пример проблемного паттерна:
yup.string().test("check-db", async function (value) {
const isTaken = await api.check(value);
return !isTaken;
});
Проблема проявляется при частых изменениях значения: ответ API относится к уже устаревшему input.
value внутри
testgetValues()YupResolver агрегирует ошибки через
ValidationError и возвращает структуру:
errors — объект ошибок по полямvalues — валидные данные (если нет ошибок)При асинхронных тестах важно учитывать, что:
Одним из базовых способов является внедрение логов прямо в
test:
yup.string().test("debug", async function (value) {
console.log("start validation", value);
const result = await api.validate(value);
console.log("end validation", value, result);
return result;
});
Такой подход позволяет выявить:
Для выявления гонок полезно сравнивать значение внутри теста с текущим состоянием формы:
test("check-current", async function (value) {
const current = this.options.context?.getValues?.("field");
if (current !== value) {
return true;
}
return await api.check(value);
});
Это позволяет “отсеивать” устаревшие проверки.
Добавление timestamp в замыкание теста позволяет отслеживать порядок выполнения:
let lastCall = 0;
yup.string().test("timestamp", async function (value) {
const callId = Date.now();
lastCall = callId;
const result = await api.check(value);
if (callId !== lastCall) {
return true;
}
return result;
});
Подход помогает выявить устаревшие асинхронные ответы.
Иногда YupResolver возвращает ошибки, которые не соответствуют текущему состоянию формы. Это происходит из-за того, что:
isValid не соответствует реальному состояниюАсинхронная валидация особенно чувствительна к частоте вызовов. При
каждом onChange может запускаться новый запрос.
Один из способов стабилизации — уменьшение частоты вызовов:
onChangemode: "onBlur"triggerОднако это снижает отзывчивость интерфейса.
Если асинхронная валидация опирается на API, поддерживающий отмену, можно снизить эффект гонок:
let controller;
yup.string().test("abortable", async function (value) {
if (controller) controller.abort();
controller = new AbortController();
const result = await fetch("/validate", {
signal: controller.signal
});
return result.ok;
});
Это не решает проблему на уровне YupResolver полностью, но уменьшает количество лишних завершённых запросов.
Асинхронная валидация должна быть максимально чистой функцией. Любые
побочные эффекты внутри test приводят к трудноотлавливаемым
состояниям:
Такие действия нарушают предсказуемость YupResolver и усложняют диагностику.
Для глубокого анализа полезно рассматривать каждый вызов
test как отдельную операцию жизненного цикла:
При наличии логов по этим этапам можно выявить:
При setValue или reset сразу для нескольких
полей YupResolver может запустить каскад асинхронных проверок.
Это приводит к:
Особенно заметно при загрузке данных формы из сервера.
Стабильная архитектура предполагает разделение:
Смешивание этих типов внутри одного test усложняет
отладку и увеличивает вероятность гонок.
React Hook Form может вызывать resolver несколько раз:
Если асинхронная логика не защищена, это приводит к множественным параллельным запросам.
Для анализа сложных случаев полезно рассматривать систему как поток событий:
Ошибка возникает на границе между async boundary и state commit, когда результат уже не соответствует актуальному input state.
На уровне архитектуры применяются следующие подходы:
Каждый подход направлен на устранение несоответствия между временем запроса и временем применения результата.