Debounce валидации в связке с YupResolver обычно рассматривается как способ контролировать частоту запуска схемной проверки в формах, построенных на react-hook-form и Yup. В стандартной конфигурации YupResolver выполняет синхронную или асинхронную валидацию при каждом изменении состояния формы, что при сложных схемах и частых обновлениях может приводить к избыточным пересчётам и деградации UX.
YupResolver выступает адаптером между схемой Yup и механизмом валидации react-hook-form. При каждом событии, которое триггерит re-validation (onChange, onBlur, onSubmit), происходит:
Важная особенность заключается в том, что Yup не оптимизирует частичные пересчёты по умолчанию на уровне resolver — схема пересчитывается в рамках заданного контекста каждый раз заново.
При частых событиях ввода это приводит к ситуации, когда один пользовательский ввод вызывает цепочку повторных вычислений.
Debounce применяется для сглаживания потока событий изменения значений формы. Основной смысл заключается в том, чтобы отложить запуск валидации до момента, когда пользователь завершит ввод.
Ключевые сценарии:
В таких случаях debounce снижает количество вызовов resolver, уменьшая нагрузку на CPU и улучшая отзывчивость интерфейса.
Один из распространённых способов — обёртывание вызова resolver в debounce-функцию.
import { useMemo } fr om "react";
import debounce fr om "lodash.debounce";
import { yupResolver } from "@hookform/resolvers/yup";
const createDebouncedResolver = (schema, delay = 300) => {
const baseResolver = yupResolver(schema);
return useMemo(() => {
return debounce(async (values, context, options) => {
return baseResolver(values, context, options);
}, delay);
}, [schema]);
};
Такой подход позволяет ограничить частоту выполнения схемной проверки, однако требует аккуратного управления жизненным циклом, так как debounce сохраняет состояние между рендерами.
react-hook-form ожидает, что resolver будет чистой функцией без побочных эффектов. При использовании debounce возникают несколько технических нюансов:
Корректная реализация должна учитывать очистку:
useEffect(() => {
return () => {
debouncedResolver.cancel();
};
}, [debouncedResolver]);
Throttle ограничивает частоту вызова валидации фиксированным интервалом, не откладывая её до остановки ввода, а выполняя периодически.
Это полезно в сценариях, где важно получать промежуточную обратную связь:
Пример интеграции:
import throttle from "lodash.throttle";
import { yupResolver } from "@hookform/resolvers/yup";
const createThrottledResolver = (schema, lim it = 200) => {
const baseResolver = yupResolver(schema);
const throttled = throttle((values, context, options, resolve) => {
resolve(baseResolver(values, context, options));
}, lim it);
return async (values, context, options) => {
return new Promise((resolve) => {
throttled(values, context, options, resolve);
});
};
};
Throttle в контексте YupResolver менее предсказуем, чем debounce, поскольку ошибки могут обновляться с фиксированным лагом независимо от поведения пользователя.
Debounce:
Throttle:
Более управляемый подход заключается в отказе от оборачивания resolver и переносе debounce на уровень конкретных input-полей.
import { useEffect } from "react";
import debounce from "lodash.debounce";
import { useForm } from "react-hook-form";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
const schema = yup.object({
email: yup.string().email().required(),
});
const resolver = yupResolver(schema);
const Form = () => {
const form = useForm({ resolver });
useEffect(() => {
const handler = debounce(() => {
form.trigger("email");
}, 400);
const subscription = form.watch((value, { name }) => {
if (name === "email") {
handler();
}
});
return () => {
handler.cancel();
subscription.unsubscribe();
};
}, [form]);
return null;
};
Такой подход сохраняет чистоту resolver и позволяет контролировать частоту валидации точечно.
Особое внимание требуется при использовании Yup tests с асинхронными операциями:
email: yup
.string()
.email()
.test("check-unique", async (value) => {
const res = await fetch(`/api/check?email=${value}`);
return res.ok;
});
Без debounce такие проверки могут вызываться на каждый символ, создавая нагрузку на сервер. В таких случаях debounce становится не оптимизацией UI, а механизмом защиты backend.
При отсутствии ограничения частоты вызовов возникают характерные проблемы:
Особенно заметно это при вложенных схемах Yup с массивами:
users: yup.array().of(
yup.object({
email: yup.string().email().required(),
profile: yup.object({
age: yup.number().min(18),
}),
})
);
Каждое изменение внутри массива приводит к полной пересборке структуры.
react-hook-form позволяет задавать режим валидации:
В сочетании с debounce можно уменьшить нагрузку без изменения resolver:
useForm({
resolver: yupResolver(schema),
mode: "onChange",
reValidateMode: "onBlur",
});
В такой конфигурации debounce часто становится избыточным, если схема не перегружена.
Использование debounce/throttle с YupResolver требует учитывать фундаментальное ограничение: resolver не предназначен для асинхронного управления потоком событий. Он рассчитан на детерминированный вызов при изменении состояния формы.
Любая попытка внедрить временную логику в resolver приводит к:
Поэтому наиболее устойчивые архитектуры переносят временную оптимизацию либо на уровень UI, либо на уровень внешних эффектов, оставляя YupResolver чистым вычислительным слоем.