Ошибки валидации, возвращаемые через связку React Hook Form и Yup,
формируются в единую структуру formState.errors. При
использовании резолвера @hookform/resolvers эти ошибки
уже нормализованы и доступны для предсказуемого отображения в UI.
Компонент ErrorMessage выступает как слой представления,
отделяющий логику формы от рендеринга ошибок. Его задача — извлечь
конкретное сообщение из структуры ошибок и корректно отобразить его в
интерфейсе.
После валидации схема Yup преобразует ошибки в объект, который React
Hook Form помещает в formState.errors:
{
email: {
type: "validation",
message: "Некорректный email"
},
profile: {
age: {
message: "Возраст должен быть числом"
}
}
}
Особенности структуры:
message;Основная функция компонента:
import { useFormContext } from "react-hook-form";
type Props = {
name: string;
};
export function ErrorMessage({ name }: Props) {
const {
formState: { errors }
} = useFormContext();
const error = name
.split(".")
.reduce((acc, key) => acc?.[key], errors as any);
if (!error?.message) return null;
return <span className="error-text">{error.message}</span>;
}
Ключевые моменты:
split(".") позволяет работать с вложенными полями
(user.email);reduce безопасно проходит по дереву ошибок;<input {...register("email")} />
<ErrorMessage name="email" />
При такой модели компонент формы не содержит логики обработки ошибок.
Для схем Yup часто используются вложенные объекты:
{
user: {
profile: {
age: number
}
}
}
Использование:
<ErrorMessage name="user.profile.age" />
Универсальный резолвер пути делает компонент независимым от структуры формы.
В случае useFieldArray структура ошибок становится
индексированной:
users[0].email
users[1].email
Рендер:
{fields.map((field, index) => (
<div key={field.id}>
<input {...register(`users.${index}.email`)} />
<ErrorMessage name={`users.${index}.email`} />
</div>
))}
Особенность:
Более строгая версия:
import { FieldErrors, useFormContext } from "react-hook-form";
type Props<T> = {
name: keyof T | string;
};
export function ErrorMessage<T>({ name }: Props<T>) {
const {
formState: { errors }
} = useFormContext<T>();
const getError = (path: string, obj: FieldErrors<T>) =>
path.split(".").reduce((acc: any, key) => acc?.[key], obj);
const error = getError(name as string, errors);
if (!error?.message) return null;
return <span>{error.message as string}</span>;
}
При использовании Controller структура может
отличаться:
import { useController, useFormContext } from "react-hook-form";
export function ErrorMessage({ name }: { name: string }) {
const { formState } = useFormContext();
const error = name
.split(".")
.reduce((acc, k) => acc?.[k], formState.errors as any);
return error?.message ? <p>{error.message}</p> : null;
}
Контролируемые компоненты не изменяют принцип доступа к ошибкам, но требуют аккуратного связывания.
В крупных формах используется единый компонент:
type ErrorProps = {
error?: { message?: string };
};
export function ErrorMessageView({ error }: ErrorProps) {
if (!error?.message) return null;
return <div role="alert">{error.message}</div>;
}
Использование:
const {
formState: { errors }
} = useFormContext();
<ErrorMessageView error={errors.email} />
Преимущество:
Ошибки должны быть доступны скринридерам:
return (
<span role="alert" aria-live="polite">
{error.message}
</span>
);
Рекомендации:
role="alert" для критических ошибок;aria-live="polite" для мягких обновлений;aria-describedby.Yup позволяет задавать сообщения на уровне схемы:
import * as yup from "yup";
const schema = yup.object({
email: yup.string().email("Неверный формат email").required("Обязательное поле")
});
ErrorMessage не должен:
Он лишь отображает message.
При наличии нескольких ошибок важно:
Пример фильтрации:
const pickMessage = (error: any) =>
Array.isArray(error) ? error[0]?.message : error?.message;
Проблема:
formState.errors часто обновляется;Оптимизация:
import { memo } from "react";
export const ErrorMessage = memo(function ErrorMessage({ name }: Props) {
const { formState } = useFormContext();
const error = name
.split(".")
.reduce((acc, key) => acc?.[key], formState.errors as any);
return error?.message ? <span>{error.message}</span> : null;
});
В дизайн-системах компонент часто оборачивается:
export function FieldError({ message }: { message?: string }) {
if (!message) return null;
return <div className="text-red-500 text-sm mt-1">{message}</div>;
}
И используется как адаптер между формой и стилями.
В зависимости от mode в React Hook Form:
onChange — ошибки обновляются в реальном времени;onBlur — после потери фокуса;onSubmit — только при отправке.ErrorMessage не зависит от режима, но его частота обновлений напрямую связана с ним.
Типичная проблема:
errors.user?.profile?.age?.message
Безопасный доступ обязателен, иначе возможны runtime-ошибки при частично заполненных структурах.
В больших формах компонент становится стандартным строительным блоком:
<FormField name="password">
<input type="password" {...register("password")} />
<ErrorMessage name="password" />
</FormField>
Это позволяет:
Ошибки API могут быть добавлены вручную:
setError("email", {
type: "server",
message: "Email уже занят"
});
ErrorMessage не различает источник ошибки — он работает только с
message.
При reset():
errors очищается;Компонент выполняет функцию адаптера между:
Он является точкой, где данные формы перестают быть моделью и становятся интерфейсом.