Библиотека @hookform/resolvers предоставляет адаптер
yupResolver, который связывает схемы валидации из Yup с
формами из React Hook Form. Одной из наиболее важных задач при работе с
типизированными формами становится корректная обработка ошибок:
получение безопасных типов, доступ к вложенным полям и создание
универсальных механизмов отображения ошибок.
После подключения yupResolver объект ошибок хранится
внутри formState.errors.
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: yupResolver(schema),
});
Тип errors автоматически строится на основе интерфейса
формы.
Пример:
type FormValues = {
email: string;
password: string;
};
const {
formState: { errors },
} = useForm<FormValues>({
resolver: yupResolver(schema),
});
Теперь:
errors.email
errors.password
имеют строгую типизацию.
Каждая ошибка представляет собой объект типа
FieldError.
Пример структуры:
{
type: "required",
message: "Поле обязательно",
ref: HTMLInputElement
}
Тип:
import { FieldError } from "react-hook-form";
Использование:
const emailError: FieldError | undefined = errors.email;
При наличии вложенных объектов структура ошибок повторяет структуру формы.
const schema = yup.object({
profile: yup.object({
firstName: yup.string().required(),
lastName: yup.string().required(),
}),
});
type FormValues = {
profile: {
firstName: string;
lastName: string;
};
};
errors.profile?.firstName?.message
errors.profile?.lastName?.message
Типы автоматически определяются как:
FieldError | undefined
const schema = yup.object({
users: yup.array(
yup.object({
email: yup.string().email().required(),
})
),
});
type FormValues = {
users: {
email: string;
}[];
};
errors.users?.[0]?.email?.message
Каждый элемент массива также типизируется автоматически.
Для полной структуры ошибок используется тип
FieldErrors<T>.
import { FieldErrors } from "react-hook-form";
type FormValues = {
email: string;
password: string;
};
const errors: FieldErrors<FormValues>;
Полезно при передаче ошибок в компоненты.
<FormInput
label="Email"
error={errors.email}
/>
import { FieldError } from "react-hook-form";
type Props = {
label: string;
error?: FieldError;
};
export const FormInput = ({ label, error }: Props) => {
return (
<div>
<label>{label}</label>
{error && (
<p>{error.message}</p>
)}
</div>
);
};
При создании переиспользуемых компонентов удобнее использовать generic-типы.
import {
FieldErrors,
FieldValues,
Path,
} from "react-hook-form";
type Props<T extends FieldValues> = {
name: Path<T>;
errors: FieldErrors<T>;
};
function InputField<T extends FieldValues>({
name,
errors,
}: Props<T>) {
return (
<div>
<p>{errors[name]?.message}</p>
</div>
);
}
errors[name] может вызывать ошибку TypeScript:
Element implicitly has an 'any' type
Причина — Path<T> может быть вложенным значением
(profile.email), а объект ошибок имеет сложную
структуру.
Распространённое решение:
npm install lodash.get
import get from "lodash.get";
const error = get(errors, name);
<p>{error?.message}</p>
Безопасная версия:
const error = get(errors, name) as FieldError | undefined;
Библиотека React Hook Form содержит готовый компонент ошибок.
npm install @hookform/error-message
import { ErrorMessage } from "@hookform/error-message";
<ErrorMessage
errors={errors}
name="email"
render={({ message }) => (
<p>{message}</p>
)}
/>
При использовании generic-типов:
type FormValues = {
email: string;
};
<ErrorMessage<FormValues>
errors={errors}
name="email"
/>
По умолчанию Yup возвращает только первую ошибку поля.
Для получения всех ошибок:
useForm({
resolver: yupResolver(schema),
criteriaMode: "all",
});
Теперь объект ошибки содержит поле types.
errors.password?.types
Пример:
{
required: "Введите пароль",
min: "Минимум 8 символов"
}
import { MultipleFieldErrors } from "react-hook-form";
Использование:
const types: MultipleFieldErrors | undefined =
errors.password?.types;
{
errors.password?.types &&
Object.values(errors.password.types).map((message) => (
<p key={message}>{message}</p>
));
}
Иногда ошибка относится не к конкретному полю, а ко всей форме.
Например:
const {
setError,
} = useForm<FormValues>();
setError("root.serverError", {
type: "server",
message: "Неверный логин или пароль",
});
errors.root?.serverError?.message
const onSub mit = async (data: FormValues) => {
const exists = await checkEmail(data.email);
if (exists) {
setError("email", {
type: "manual",
message: "Email уже используется",
});
return;
}
};
Yup выбрасывает объект ValidationError.
import { ValidationError } from "yup";
{
name: "ValidationError",
path: "email",
message: "Некорректный email",
errors: [],
inner: []
}
Иногда схема валидируется вне React Hook Form.
try {
await schema.validate(data, {
abortEarly: false,
});
} catch (error) {
if (error instanceof ValidationError) {
console.log(error.errors);
}
}
При abortEarly: false Yup собирает все ошибки.
error.inner
Пример:
[
{
path: "email",
message: "Введите email"
},
{
path: "password",
message: "Минимум 8 символов"
}
]
const formatted = error.inner.reduce(
(acc, current) => {
if (current.path) {
acc[current.path] = current.message;
}
return acc;
},
{} as Record<string, string>
);
type ErrorMap<T> = Partial<
Record<keyof T, string>
>;
function mapYupErrors<T>(
error: ValidationError
): ErrorMap<T> {
return error.inner.reduce((acc, current) => {
if (current.path) {
acc[current.path as keyof T] =
current.message;
}
return acc;
}, {} as ErrorMap<T>);
}
Ошибки всегда могут отсутствовать.
Неправильно:
errors.email.message
Правильно:
errors.email?.message
<p>
{errors.email?.message ?? " "}
</p>
const schema = yup.object({
middleName: yup.string().optional(),
});
Даже если поле необязательное, ошибка всё равно может появиться.
Например:
middleName: yup
.string()
.max(10)
.optional()
const schema = yup.object({
password: yup
.string()
.test(
"has-uppercase",
"Нужна заглавная буква",
(value) => {
return /[A-Z]/.test(value || "");
}
),
});
Ошибка будет иметь тип:
type: "has-uppercase"
if (errors.password?.type === "has-uppercase") {
console.log("Ошибка регистра");
}
yup.setLocale({
mixed: {
required: "Поле обязательно",
},
});
Все сообщения остаются строками:
message?: string
enum ValidationErrorType {
REQUIRED = "required",
MIN = "min",
MAX = "max",
}
if (
errors.password?.type ===
ValidationErrorType.MIN
) {
console.log("Слишком короткий пароль");
}
function getErrorMessage(
error?: FieldError
) {
return error?.message || "";
}
function getArrayError(
errors: FieldErrors<FormValues>,
index: number
) {
return errors.users?.[index]?.email?.message;
}
<TextField
error={!!errors.email}
helperText={errors.email?.message}
/>
<FormErrorMessage>
{errors.email?.message}
</FormErrorMessage>
Yup может изменять значения через transform.
yup.string().transform((value) => {
return value.trim();
});
Ошибка будет относиться уже к преобразованному значению.
schema.validate(data, {
strict: true,
});
Трансформации отключаются, что может изменить поведение ошибок.
schema.validate(data, {
abortEarly: true,
});
Только первая ошибка.
schema.validate(data, {
abortEarly: false,
});
Все ошибки одновременно.
abortEarly: false особенно полезен:
console.log(errors);
catch (error) {
console.log(JSON.stringify(error, null, 2));
}
В TypeScript переменная error имеет тип
unknown.
Правильная проверка:
catch (error) {
if (error instanceof ValidationError) {
console.log(error.message);
}
}
Yup умеет автоматически строить типы.
type FormValues = yup.InferType<typeof schema>;
Теперь ошибки синхронизированы со схемой.
Без InferType:
type FormValues = {
email: string;
};
Схема и типы могут разойтись.
С InferType типы всегда соответствуют Yup-схеме.
import * as yup from "yup";
import {
useForm,
FieldError,
} from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
const schema = yup.object({
email: yup
.string()
.email("Некорректный email")
.required("Введите email"),
password: yup
.string()
.min(8, "Минимум 8 символов")
.required("Введите пароль"),
});
type FormValues = yup.InferType<typeof schema>;
export default function App() {
const {
register,
handleSubmit,
setError,
formState: { errors },
} = useForm<FormValues>({
resolver: yupResolver(schema),
criteriaMode: "all",
});
const onSub mit = async (
data: FormValues
) => {
try {
await login(data);
} catch {
setError("root.serverError", {
type: "server",
message: "Ошибка авторизации",
});
}
};
return (
<form onSub mit={handleSubmit(onSubmit)}>
<input {...register("email")} />
<p>{errors.email?.message}</p>
<input
type="password"
{...register("password")}
/>
<p>{errors.password?.message}</p>
<p>
{
errors.root?.serverError?.message
}
</p>
<button type="submit">
Войти
</button>
</form>
);
}