YupResolver чаще всего используется совместно с
библиотеками управления формами, особенно с React Hook Form. Однако
прямое подключение резолвера подходит не для всех сценариев. В реальных
проектах появляются дополнительные требования:
Для решения подобных задач создаются wrapper-функции —
функции-обёртки над yupResolver.
Базовая идея выглядит так:
const resolver = customYupResolver(schema);
Вместо прямого:
const resolver = yupResolver(schema);
Минимальная обёртка:
import { yupResolver } from "@hookform/resolvers/yup";
export function createResolver(schema) {
return yupResolver(schema);
}
На первый взгляд такая конструкция бесполезна, однако именно она становится точкой расширения.
Во многих приложениях требуется единое поведение для всех схем.
Пример:
import { yupResolver } from "@hookform/resolvers/yup";
export function createResolver(schema) {
return yupResolver(schema, {
abortEarly: false,
stripUnknown: true,
});
}
Теперь каждая форма автоматически:
Wrapper удобно использовать для подключения переводов.
import * as yup from "yup";
yup.setLocale({
mixed: {
required: "Поле обязательно",
},
string: {
email: "Некорректный email",
min: "Слишком короткое значение",
},
});
Обёртка:
import { yupResolver } from "@hookform/resolvers/yup";
export function localizedResolver(schema) {
return yupResolver(schema);
}
После этого все формы автоматически получают локализованные сообщения.
Во время разработки полезно видеть ошибки валидации централизованно.
import { yupResolver } from "@hookform/resolvers/yup";
export function debugResolver(schema) {
const resolver = yupResolver(schema);
return async (data, context, options) => {
const result = await resolver(data, context, options);
if (Object.keys(result.errors).length > 0) {
console.log("Validation errors:", result.errors);
}
return result;
};
}
Использование:
const resolver = debugResolver(schema);
Иногда стандартный формат ошибок неудобен.
Например, сервер ожидает:
{
"field": "email",
"message": "Invalid email"
}
Вместо вложенного объекта.
import { yupResolver } from "@hookform/resolvers/yup";
export function flatErrorResolver(schema) {
const resolver = yupResolver(schema);
return async (data, context, options) => {
const result = await resolver(data, context, options);
const flatErrors = Object.entries(result.errors).map(
([field, error]) => ({
field,
message: error.message,
})
);
return {
values: result.values,
errors: flatErrors,
};
};
}
Иногда схема зависит от роли пользователя.
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
export function roleBasedResolver(role) {
let schema;
if (role === "admin") {
schema = yup.object({
name: yup.string().required(),
accessLevel: yup.number().required(),
});
} else {
schema = yup.object({
name: yup.string().required(),
});
}
return yupResolver(schema);
}
Обёртка может менять поведение в зависимости от окружения.
import { yupResolver } from "@hookform/resolvers/yup";
export function productionResolver(schema) {
if (process.env.NODE_ENV !== "production") {
return async (data) => ({
values: data,
errors: {},
});
}
return yupResolver(schema);
}
Подобный подход иногда применяется при тестировании интерфейсов.
До запуска схемы данные можно модифицировать.
import { yupResolver } from "@hookform/resolvers/yup";
export function sanitizeResolver(schema) {
const resolver = yupResolver(schema);
return async (data, context, options) => {
const sanitizedData = Object.fromEntries(
Object.entries(data).map(([key, value]) => [
key,
typeof value === "string"
? value.trim()
: value,
])
);
return resolver(sanitizedData, context, options);
};
}
Иногда перед валидацией необходимо получить внешние данные.
import { yupResolver } from "@hookform/resolvers/yup";
export function blacklistResolver(schema, blacklistService) {
const resolver = yupResolver(schema);
return async (data, context, options) => {
const blacklist = await blacklistService.getEmails();
if (blacklist.includes(data.email)) {
return {
values: {},
errors: {
email: {
type: "manual",
message: "Email запрещён",
},
},
};
}
return resolver(data, context, options);
};
}
Крупные формы часто собираются из модулей.
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
export function mergeSchemas(...schemas) {
const merged = schemas.reduce(
(acc, schema) => acc.concat(schema),
yup.object()
);
return yupResolver(merged);
}
Использование:
const resolver = mergeSchemas(
profileSchema,
addressSchema,
securitySchema
);
В enterprise-приложениях часть полей может зависеть от фич.
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
export function featureResolver(flags) {
let schema = yup.object({
name: yup.string().required(),
});
if (flags.enablePhone) {
schema = schema.shape({
phone: yup.string().required(),
});
}
return yupResolver(schema);
}
Создание сложных схем может быть дорогой операцией.
const cache = new Map();
export function cachedResolver(key, schemaFactory) {
if (!cache.has(key)) {
cache.set(key, schemaFactory());
}
return cache.get(key);
}
Использование:
const resolver = cachedResolver(
"registration",
() => yupResolver(createSchema())
);
В TypeScript wrapper-функции особенно полезны.
import { yupResolver } from "@hookform/resolvers/yup";
import { AnyObjectSchema } from "yup";
export function createTypedResolver<T>(
schema: AnyObjectSchema
) {
return yupResolver<T>(schema);
}
Часто требуется объединить серверные ошибки и ошибки Yup.
import { yupResolver } from "@hookform/resolvers/yup";
export function apiAwareResolver(schema, apiErrors) {
const resolver = yupResolver(schema);
return async (data, context, options) => {
const result = await resolver(data, context, options);
for (const error of apiErrors) {
result.errors[error.field] = {
type: "server",
message: error.message,
};
}
return result;
};
}
Многошаговые формы часто требуют отдельную схему на каждый этап.
import { yupResolver } from "@hookform/resolvers/yup";
export function stepResolver(step, schemas) {
return yupResolver(schemas[step]);
}
Использование:
const resolver = stepResolver(currentStep, {
1: userSchema,
2: addressSchema,
3: paymentSchema,
});
Иногда схема может отсутствовать.
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
export function safeResolver(schema) {
const fallback = yup.object({});
return yupResolver(schema || fallback);
}
При сложной валидации полезно измерять производительность.
import { yupResolver } from "@hookform/resolvers/yup";
export function profilingResolver(schema) {
const resolver = yupResolver(schema);
return async (data, context, options) => {
const start = performance.now();
const result = await resolver(data, context, options);
const end = performance.now();
console.log(`Validation time: ${end - start}ms`);
return result;
};
}
Ошибки формы могут отправляться в систему мониторинга.
import { yupResolver } from "@hookform/resolvers/yup";
export function analyticsResolver(schema, analytics) {
const resolver = yupResolver(schema);
return async (data, context, options) => {
const result = await resolver(data, context, options);
if (Object.keys(result.errors).length > 0) {
analytics.track("validation_error", result.errors);
}
return result;
};
}
import { yupResolver } from "@hookform/resolvers/yup";
export function normalizeResolver(schema) {
const resolver = yupResolver(schema);
return async (data, context, options) => {
const normalized = {
...data,
email: data.email?.toLowerCase(),
};
return resolver(normalized, context, options);
};
}
Обёртки можно объединять.
export function composeResolvers(...wrappers) {
return (schema) =>
wrappers.reduce(
(acc, wrapper) => wrapper(acc),
schema
);
}
Использование:
const resolver = composeResolvers(
sanitizeResolver,
debugResolver,
profilingResolver
)(schema);
Иногда обёртки реализуются по принципу middleware.
export function middlewareResolver(schema, middlewares) {
let resolver = yupResolver(schema);
for (const middleware of middlewares.reverse()) {
resolver = middleware(resolver);
}
return resolver;
}
Middleware:
function loggerMiddleware(resolver) {
return async (data, context, options) => {
console.log("Before validation");
const result = await resolver(
data,
context,
options
);
console.log("After validation");
return result;
};
}
Неправильно:
return async (data) => {
return resolver(data);
};
Правильно:
return async (data, context, options) => {
return resolver(data, context, options);
};
Некоторые библиотеки ожидают стандартный формат ошибок
React Hook Form.
Опасный вариант:
return {
errors: [],
};
Без совместимого интерфейса форма перестанет работать.
Плохая практика:
return yupResolver(createSchema());
при каждом рендере компонента.
Лучше:
const schema = createSchema();
const resolver = yupResolver(schema);
Wrapper не должен превращаться в сервис приложения.
Плохо:
if (user.balance < 0) {
banUser();
}
Резолвер отвечает только за подготовку и проверку данных.
export function createAppResolver(schema) {
return profilingResolver(
sanitizeResolver(
debugResolver(schema)
)
);
}
Полезно разделять:
Wrapper-функции позволяют сохранять такую архитектуру чистой.
Желательно избегать:
Один из самых распространённых подходов.
export function createResolverFactory(config) {
return function(schema) {
return yupResolver(schema, {
abortEarly: config.abortEarly,
stripUnknown: config.stripUnknown,
});
};
}
Использование:
const resolverFactory = createResolverFactory({
abortEarly: false,
stripUnknown: true,
});
const resolver = resolverFactory(schema);
Yup поддерживает lazy.
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
export function lazyResolver(factory) {
const schema = yup.lazy(factory);
return yupResolver(schema);
}
import { yupResolver } from "@hookform/resolvers/yup";
export function modeResolver(schema, mode) {
const strict = mode === "strict";
return yupResolver(schema, {
strict,
});
}
import { yupResolver } from "@hookform/resolvers/yup";
export function hybridResolver(schema, manualValidate) {
const resolver = yupResolver(schema);
return async (data, context, options) => {
const result = await resolver(data, context, options);
const manualErrors = manualValidate(data);
return {
values: result.values,
errors: {
...result.errors,
...manualErrors,
},
};
};
}