ErrorMessage компонент

Ошибки валидации, возвращаемые через связку React Hook Form и Yup, формируются в единую структуру formState.errors. При использовании резолвера @hookform/resolvers эти ошибки уже нормализованы и доступны для предсказуемого отображения в UI.

Компонент ErrorMessage выступает как слой представления, отделяющий логику формы от рендеринга ошибок. Его задача — извлечь конкретное сообщение из структуры ошибок и корректно отобразить его в интерфейсе.


Структура ошибок, приходящих из YupResolver

После валидации схема Yup преобразует ошибки в объект, который React Hook Form помещает в formState.errors:

{
  email: {
    type: "validation",
    message: "Некорректный email"
  },
  profile: {
    age: {
      message: "Возраст должен быть числом"
    }
  }
}

Особенности структуры:

  • вложенность соответствует форме данных;
  • каждое поле содержит message;
  • массивы и динамические поля используют индексы;
  • ошибки могут отсутствовать на промежуточных уровнях.

Базовая задача ErrorMessage

Основная функция компонента:

  • безопасно достать сообщение ошибки по пути поля;
  • не ломать рендер при отсутствии ошибки;
  • обеспечить единый формат вывода;
  • минимизировать дублирование логики в компонентах формы.

Простейшая реализация ErrorMessage

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 безопасно проходит по дереву ошибок;
  • компонент ничего не рендерит при отсутствии ошибки.

Использование с register

<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>
))}

Особенность:

  • индексы должны совпадать с реальным порядком массива;
  • при удалении элементов важно не терять синхронизацию.

Универсальный типобезопасный вариант (TypeScript)

Более строгая версия:

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>;
}

Инкапсуляция через useController

При использовании 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} />

Преимущество:

  • отсутствие логики парсинга пути;
  • максимальная читаемость;
  • удобство тестирования.

Поддержка accessibility

Ошибки должны быть доступны скринридерам:

return (
  <span role="alert" aria-live="polite">
    {error.message}
  </span>
);

Рекомендации:

  • role="alert" для критических ошибок;
  • aria-live="polite" для мягких обновлений;
  • связывание через aria-describedby.

Связь с Yup схемой и кастомизация сообщений

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;
});

Интеграция с UI-слоем

В дизайн-системах компонент часто оборачивается:

export function FieldError({ message }: { message?: string }) {
  if (!message) return null;

  return <div className="text-red-500 text-sm mt-1">{message}</div>;
}

И используется как адаптер между формой и стилями.


Поведение при режиме validation

В зависимости от 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>

Это позволяет:

  • стандартизировать вывод ошибок;
  • упростить поддержку;
  • снизить количество boilerplate-кода.

Обработка кастомных ошибок сервера

Ошибки API могут быть добавлены вручную:

setError("email", {
  type: "server",
  message: "Email уже занят"
});

ErrorMessage не различает источник ошибки — он работает только с message.


Поведение при очистке формы

При reset():

  • errors очищается;
  • ErrorMessage автоматически исчезает;
  • дополнительная логика не требуется.

Архитектурная роль ErrorMessage

Компонент выполняет функцию адаптера между:

  • структурой Yup-валидации;
  • системой управления формой React Hook Form;
  • UI-слоем отображения.

Он является точкой, где данные формы перестают быть моделью и становятся интерфейсом.