Проверка данных во время выполнения — одна из ключевых задач при
работе с формами, API, пользовательским вводом и динамическими
структурами объектов. В экосистеме React наиболее распространённой
связкой является библиотека Yup
совместно с React Hook
Form и адаптером @hookform/resolvers, внутри
которого используется yupResolver.
yupResolver выполняет роль промежуточного слоя между
системой валидации Yup и механизмом управления формой React Hook Form.
Основная задача резолвера — запуск анализа схемы в runtime и
преобразование результата проверки в формат, понятный React Hook
Form.
Во время выполнения приложения данные проходят несколько стадий обработки:
yupResolverСхематично процесс выглядит так:
form values
↓
yupResolver
↓
schema.validate()
↓
ValidationError | validated data
↓
React Hook Form
Базовый пример подключения:
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
const schema = yup.object({
email: yup.string().email().required(),
age: yup.number().min(18)
});
const form = useForm({
resolver: yupResolver(schema)
});
Внутри yupResolver вызывается метод:
schema.validate(values, options)
или:
schema.validateSync(values, options)
в зависимости от режима работы.
Yup анализирует схему динамически во время выполнения приложения. Это означает:
Пример динамического анализа:
const schema = yup.object({
role: yup.string().required(),
permissions: yup.array().when("role", {
is: "admin",
then: (schema) => schema.min(1).required(),
otherwise: (schema) => schema.notRequired()
})
});
Во время runtime Yup:
role;await schema.validate(data);
Используется по умолчанию в yupResolver.
Подходит для:
schema.validateSync(data);
Работает быстрее, но не поддерживает async-тесты.
По умолчанию Yup прекращает проверку после первой ошибки.
schema.validate(data, {
abortEarly: true
});
Для получения всех ошибок:
schema.validate(data, {
abortEarly: false
});
В React Hook Form обычно используется именно этот режим.
Удаляет поля, отсутствующие в схеме.
const schema = yup.object({
name: yup.string()
});
const result = await schema.validate(
{
name: "Alex",
secret: "hidden"
},
{
stripUnknown: true
}
);
console.log(result);
Результат:
{
name: "Alex"
}
Управляет глубиной анализа вложенных объектов.
schema.validate(data, {
recursive: false
});
При отключении вложенные структуры не проверяются.
Отключает автоматическое преобразование типов.
const schema = yup.number();
await schema.validate("42");
Результат:
42
В strict-режиме:
await schema.validate("42", {
strict: true
});
Ошибка:
this must be a `number` type
Перед основной проверкой Yup выполняет трансформацию значений.
const schema = yup.number();
const value = schema.cast("100");
console.log(value);
Результат:
100
Это важнейшая часть runtime-анализа.
raw value
↓
transform
↓
cast
↓
validation
const schema = yup.string().transform((value) => {
return value.trim();
});
Каждое значение проходит через pipeline трансформаций.
const schema = yup.string()
.transform((value) => value.trim())
.transform((value) => value.toLowerCase());
Runtime-цепочка:
" ADMIN "
↓
trim()
↓
"ADMIN"
↓
toLowerCase()
↓
"admin"
const schema = yup.object({
user: yup.object({
profile: yup.object({
name: yup.string().required()
})
})
});
Yup рекурсивно обходит структуру:
root
└── user
└── profile
└── name
const schema = yup.array(
yup.object({
title: yup.string().required()
})
);
Во время runtime Yup:
[
{ title: "A" },
{ title: "" }
]
Ошибка:
[1].title is a required field
При ошибке Yup генерирует объект ValidationError.
Структура:
{
name: "ValidationError",
path: "email",
message: "email is required",
errors: [],
inner: []
}
При abortEarly: false Yup собирает полный список
ошибок.
try {
await schema.validate(data, {
abortEarly: false
});
} catch (error) {
console.log(error.inner);
}
Пример:
[
{
path: "email",
message: "Invalid email"
},
{
path: "password",
message: "Too short"
}
]
React Hook Form использует собственный формат ошибок.
yupResolver преобразует:
ValidationError
в:
{
email: {
type: "email",
message: "Invalid email"
}
}
Внутренне резолвер:
for (const error of validationError.inner) {
errors[error.path] = {
type: error.type,
message: error.message
};
}
Yup поддерживает контекст выполнения.
const schema = yup.object({
price: yup.number().test(
"max-price",
"Too expensive",
function(value) {
return value <= this.options.context.maxPrice;
}
)
});
Передача контекста:
yupResolver(schema, {
context: {
maxPrice: 1000
}
});
Внутри test() доступен специальный runtime-контекст.
test(function(value) {
console.log(this.path);
console.log(this.parent);
console.log(this.options);
});
const schema = yup.object({
password: yup.string().required(),
confirmPassword: yup.string()
.oneOf(
[yup.ref("password")],
"Passwords mismatch"
)
});
Во время проверки Yup:
password;lazy() создаёт схему динамически.
const schema = yup.lazy((value) => {
if (typeof value === "string") {
return yup.string();
}
return yup.number();
});
Runtime-процесс:
incoming value
↓
lazy resolver
↓
dynamic schema
↓
validation
const schema = yup.object({
type: yup.string().required(),
payload: yup.lazy((value, options) => {
const type = options.parent.type;
if (type === "email") {
return yup.object({
email: yup.string().email()
});
}
return yup.object({
phone: yup.string()
});
})
});
const baseSchema = yup.object({
id: yup.number().required()
});
const extendedSchema = baseSchema.concat(
yup.object({
title: yup.string().required()
})
);
Во время выполнения Yup объединяет AST схем.
Метод describe() позволяет получить runtime-описание
схемы.
const description = schema.describe();
console.log(description);
Результат:
{
type: "object",
fields: {
email: {
type: "string"
}
}
}
describe() полезен для:
Позволяет извлекать часть схемы.
const nameSchema = yup.reach(
schema,
"user.profile.name"
);
await schema.validateAt(
"user.email",
data
);
Проверяется только конкретное поле.
Плохая практика:
function Component() {
const schema = yup.object({
name: yup.string()
});
}
Схема пересоздаётся при каждом рендере.
Оптимизация:
const schema = yup.object({
name: yup.string()
});
или:
const schema = useMemo(() => {
return yup.object({
name: yup.string()
});
}, []);
На производительность влияют:
when;transform;Пример тяжёлой проверки:
const schema = yup.array(
yup.object({
items: yup.array(
yup.object({
name: yup.string().required()
})
)
})
);
Глубокие структуры увеличивают:
const schema = yup.string().test(
"unique-email",
"Email already exists",
async (value) => {
const exists = await api.checkEmail(value);
return !exists;
}
);
Во время runtime Yup:
yup.string()
.required()
.min(5)
.matches(/[A-Z]/)
Runtime-цепочка:
required
↓
min
↓
matches
Yup не поддерживает встроенную отмену async-проверок.
Проблема:
user typing
↓
multiple async validations
↓
race conditions
Особенно критично при:
yupResolver вызывается:
React Hook Form ожидает:
{
values,
errors
}
Успешная проверка:
{
values: validatedValues,
errors: {}
}
Ошибка:
{
values: {},
errors: {
email: {
message: "Required"
}
}
}
resolver: yupResolver(schema)
resolver: yupResolver(schema, {}, {
mode: "sync"
})
Yup активно приводит типы.
const schema = yup.boolean();
schema.cast("true");
Результат:
true
yup.number().cast("");
Результат:
NaN
Это важная runtime-особенность Yup.
yup.string().nullable();
Разрешает:
null
но не:
undefined
yup.string().optional();
Разрешает отсутствие значения.
yup.string().defined();
Запрещает:
undefined
yup.string().oneOf([
"admin",
"user",
"guest"
]);
Во время проверки Yup использует внутреннее сравнение через список допустимых значений.
yup.string().notOneOf([
"root",
"system"
]);
yup.string().matches(
/^[A-Z]{3}\d+$/
);
Проверка происходит через стандартный RegExp.test().
const schema = yup.string().meta({
placeholder: "Enter email"
});
Получение:
schema.describe().meta
const schema = yup.object({
role: yup.string().default("user")
});
schema.getDefault();
Результат:
{
role: "user"
}
const cloned = schema.clone();
Создаёт независимую копию runtime-схемы.
Yup хранит:
Каждая схема представляет собой объект с цепочкой runtime-конфигурации.
Общий процесс:
input
↓
cast()
↓
transform()
↓
type check
↓
conditions
↓
tests
↓
ValidationError | success
yup.string().when("role", {
is: "admin",
then: (schema) => schema.required()
});
Во время выполнения Yup:
Некорректная схема:
fieldA.when("fieldB")
fieldB.when("fieldA")
Может приводить к рекурсивным проблемам анализа.
schema.validateSyncAt(
"user.name",
data
);
Полезно при локализации ошибок.
console.log(
JSON.stringify(
schema.describe(),
null,
2
)
);
Позволяет исследовать итоговую runtime-структуру.
Yup выполняет проверку только в runtime.
TypeScript работает исключительно во время компиляции.
Поэтому возможна ситуация:
type User = {
age: number;
}
и:
{
age: "wrong"
}
TypeScript не предотвратит ошибку при получении внешних данных.
Runtime-валидация Yup решает эту проблему.
type User = yup.InferType<typeof schema>;
Тип выводится из runtime-схемы.
Yup имеет ряд архитектурных ограничений:
Крупные приложения обычно разделяют:
schemas/
auth/
profile/
billing/
admin/
и создают:
const phoneValidator = yup.string()
.matches(/^\+\d+$/);
const userSchema = yup.object({
phone: phoneValidator
});
function createUserSchema(options) {
return yup.object({
name: yup.string()
.max(options.maxNameLength)
});
}
Runtime-конфигурация позволяет создавать динамические схемы под разные окружения.
React Hook Form поддерживает разные режимы:
useForm({
mode: "onChange"
});
В этом случае yupResolver вызывается при каждом
изменении значения.
Последствия:
Для снижения нагрузки применяют:
validateAt;Yup может использоваться:
Одна схема может работать в нескольких runtime-средах.
Общая схема:
shared/
validation/
userSchema.js
Используется одновременно:
Некорректные схемы тоже могут генерировать ошибки выполнения.
Пример:
yup.object({
age: yup.number().min("wrong")
});
Ошибка возникает непосредственно в runtime.
yup.addMethod(
yup.string,
"isHexColor",
function() {
return this.matches(
/^#([0-9A-F]{3}){1,2}$/i
);
}
);
Использование:
yup.string().isHexColor();
Механизм addMethod() изменяет prototype-систему Yup во
время выполнения приложения.
Это позволяет:
Полный lifecycle:
form event
↓
resolver execution
↓
schema cast
↓
condition resolving
↓
transform pipeline
↓
validation tests
↓
ValidationError mapping
↓
React Hook Form state update