В экосистеме react-hook-form механизм resolver выступает
промежуточным слоем между формой и валидационной схемой. Он преобразует
входные данные в формат, пригодный для проверки, запускает валидацию и
возвращает структурированный результат, содержащий либо валидированные
значения, либо ошибки.
YupResolver представляет собой адаптер, связывающий
схемы валидации библиотеки Yup с механизмом resolver. Однако встроенная
реализация не всегда покрывает сложные сценарии: динамические схемы,
частичные проверки, каскадные зависимости полей, условную логику и
гибридные источники правил. В таких случаях используется создание
собственных resolver-функций.
Resolver-функция в контексте react-hook-form имеет
строго определённую сигнатуру:
values формыСтруктура результата:
values — нормализованные данные формыerrors — объект ошибок, сопоставленный с полямиОбобщённая форма:
const customResolver = async (values, context, options) => {
return {
values: {},
errors: {}
};
};
Ключевой момент заключается в том, что resolver может быть как синхронным, так и асинхронным, что критично для интеграции с Yup, учитывающим промисы валидации.
Простейшая реализация resolver поверх Yup выглядит как прямой вызов схемы:
import * as yup from "yup";
const schema = yup.object({
email: yup.string().email().required(),
age: yup.number().min(18)
});
const customYupResolver = async (values) => {
try {
const validatedValues = await schema.validate(values, {
abortEarly: false,
stripUnknown: true
});
return {
values: validatedValues,
errors: {}
};
} catch (validationError) {
const errors = {};
validationError.inner.forEach((err) => {
if (!err.path) return;
errors[err.path] = {
type: err.type ?? "validation",
message: err.message
};
});
return {
values: {},
errors
};
}
};
Данная структура отражает ключевой принцип: Yup возвращает массив ошибок, который требуется преобразовать в плоскую карту ошибок формы.
Yup формирует ошибки в виде массива inner, где каждая
ошибка содержит:
path — путь к полюmessage — текст ошибкиtype — тип нарушенияОднако react-hook-form ожидает объект с вложенной
структурой ошибок:
{
fieldName: {
type: string,
message: string
}
}
При сложных структурах данных (вложенные объекты, массивы) требуется трансформация путей:
const setNestedError = (errors, path, error) => {
const keys = path.split(".");
let current = errors;
keys.forEach((key, index) => {
if (index === keys.length - 1) {
current[key] = error;
} else {
current[key] = current[key] || {};
current = current[key];
}
});
};
Такой подход позволяет корректно обрабатывать поля вида:
user.emailaddresses[0].cityprofile.settings.themeОдной из причин написания собственного resolver является необходимость динамического формирования схемы:
Пример динамической схемы:
const getSchema = (mode) => {
return yup.object({
name: yup.string().required(),
company: mode === "business"
? yup.string().required()
: yup.string().notRequired()
});
};
Resolver в этом случае строится на лету:
const dynamicResolver = async (values, context) => {
const schema = getSchema(context.mode);
try {
const result = await schema.validate(values, {
abortEarly: false
});
return {
values: result,
errors: {}
};
} catch (err) {
const errors = {};
err.inner.forEach((e) => {
errors[e.path] = {
type: e.type,
message: e.message
};
});
return {
values: {},
errors
};
}
};
В реальных формах полная валидация на каждом изменении может быть избыточной. Resolver может учитывать режим:
onSubmitonChangeonBlurОптимизация достигается через управление глубиной проверки:
const optimizedResolver = async (values, context, options) => {
const shouldValidateAll = options?.criteriaMode === "all";
const schema = yup.object({
email: yup.string().email(),
password: yup.string()
});
try {
const result = await schema.validate(values, {
abortEarly: !shouldValidateAll
});
return {
values: result,
errors: {}
};
} catch (err) {
const errors = {};
err.inner.forEach((e) => {
if (!errors[e.path]) {
errors[e.path] = {
type: e.type,
message: e.message
};
}
});
return {
values: {},
errors
};
}
};
Перед передачей данных в Yup часто требуется нормализация:
undefinedconst normalizeValues = (values) => {
const result = {};
Object.keys(values).forEach((key) => {
const value = values[key];
if (value === "") {
result[key] = undefined;
return;
}
if (typeof value === "string" && !isNaN(value)) {
result[key] = Number(value);
return;
}
result[key] = value;
});
return result;
};
Resolver становится точкой контроля данных до валидации:
const resolver = async (values) => {
const normalized = normalizeValues(values);
try {
const result = await schema.validate(normalized, {
abortEarly: false
});
return {
values: result,
errors: {}
};
} catch (e) {
const errors = {};
e.inner.forEach((err) => {
errors[err.path] = {
type: err.type,
message: err.message
};
});
return {
values: {},
errors
};
}
};
При частых изменениях формы возможно повторное вычисление одинаковых состояний. Ввод кэширования снижает нагрузку:
const cache = new Map();
const hashValues = (values) => JSON.stringify(values);
const cachedResolver = async (values) => {
const hash = hashValues(values);
if (cache.has(hash)) {
return cache.get(hash);
}
try {
const result = await schema.validate(values, {
abortEarly: false
});
const response = {
values: result,
errors: {}
};
cache.set(hash, response);
return response;
} catch (e) {
const errors = {};
e.inner.forEach((err) => {
errors[err.path] = {
type: err.type,
message: err.message
};
});
const response = {
values: {},
errors
};
cache.set(hash, response);
return response;
}
};
Сложные формы часто разбиваются на независимые схемы, которые затем объединяются в resolver.
const userSchema = yup.object({
name: yup.string().required()
});
const profileSchema = yup.object({
age: yup.number().required()
});
const composedResolver = async (values) => {
try {
const user = await userSchema.validate(values);
const profile = await profileSchema.validate(values);
return {
values: {
...user,
...profile
},
errors: {}
};
} catch (e) {
const errors = {};
e.inner?.forEach((err) => {
errors[err.path] = {
type: err.type,
message: err.message
};
});
return {
values: {},
errors
};
}
};
Yup активно используется для массивов объектов, что требует корректной обработки путей:
users[0].emailitems[3].priceПри формировании ошибок важно сохранять индексную структуру:
err.inner.forEach((error) => {
const path = error.path.replace(/\[(\d+)\]/g, ".$1");
errors[path] = {
type: error.type,
message: error.message
};
});
Такая нормализация упрощает последующую работу с состоянием формы.
Resolver может включать внешние проверки:
const asyncResolver = async (values) => {
const schema = yup.object({
email: yup.string().email().required()
});
const emailExists = await api.checkEmail(values.email);
try {
await schema.validate(values, { abortEarly: false });
const errors = {};
if (emailExists) {
errors.email = {
type: "exists",
message: "Email уже используется"
};
}
return {
values,
errors
};
} catch (e) {
const errors = {};
e.inner.forEach((err) => {
errors[err.path] = {
type: err.type,
message: err.message
};
});
return {
values: {},
errors
};
}
};
Такой подход объединяет схему Yup и произвольную бизнес-логику в одном resolver-слое.
При комбинировании нескольких источников ошибок возникает необходимость определения приоритетов:
const mergeErrors = (schemaErrors, businessErrors) => {
return {
...schemaErrors,
...Object.keys(businessErrors).reduce((acc, key) => {
acc[key] = {
type: "business",
message: businessErrors[key]
};
return acc;
}, {})
};
};
Такая композиция обеспечивает предсказуемую модель отображения ошибок в форме.
Resolver можно оборачивать для расширения функциональности:
const withLogging = (resolver) => {
return async (values, context, options) => {
const start = Date.now();
const result = await resolver(values, context, options);
const duration = Date.now() - start;
logger.log("validation", { duration });
return result;
};
};
Подобная декорация позволяет внедрять:
без изменения основной логики валидации.