Обработка Ошибок
Стратегии эффективной обработки и отображения ошибок валидации.
Базовое Отображение Ошибок
Встроенные Ошибки Полей
Показывайте ошибки под каждым полем:
import { useFormControl } from '@reformer/core';
import { FieldNode } from '@reformer/core';
interface TextFieldProps {
field: FieldNode<string>;
label: string;
}
export function TextField({ field, label }: TextFieldProps) {
const control = useFormControl(field);
const showError = control.touched && control.invalid;
return (
<div className="text-field">
<label>{label}</label>
<input
value={control.value ?? ''}
onChange={(e) => control.setValue(e.target.value)}
onBlur={() => control.markAsTouched()}
className={showError ? 'error' : ''}
/>
{showError && control.errors && (
<span className="error-message">{getErrorMessage(control.errors)}</span>
)}
</div>
);
}
Преобразователь Сообщений об Ошибках
Централизованная обработка сообщений об ошибках:
utils/error-messages.ts
export type ErrorKey =
| 'required'
| 'email'
| 'minLength'
| 'maxLength'
| 'min'
| 'max'
| 'pattern'
| 'usernameTaken'
| 'emailTaken'
| 'passwordMismatch';
export const errorMessages: Record<ErrorKey, (params: any) => string> = {
required: () => 'Это поле обязательно',
email: () => 'Пожалуйста, введите корректный адрес электронной почты',
minLength: (p) => `Минимум ${p.required} символов`,
maxLength: (p) => `Максимум ${p.required} символов`,
min: (p) => `Минимум ${p.min}`,
max: (p) => `Максимум ${p.max}`,
pattern: (p) => p.message || 'Неверный формат',
usernameTaken: () => 'Это имя пользователя уже занято',
emailTaken: () => 'Этот email уже зарегистрирован',
passwordMismatch: () => 'Пароли не совпадают',
};
export function getErrorMessage(errors: Record<string, any>): string {
const [key, params] = Object.entries(errors)[0];
const getMessage = errorMessages[key as ErrorKey];
return getMessage ? getMessage(params) : 'Неверное значение';
}
Обработка Ошибок на Уровне Поля
Отображение Множественных Ошибок
Показывайте все ошибки для поля:
interface ErrorListProps {
errors: Record<string, any>;
}
export function ErrorList({ errors }: ErrorListProps) {
if (!errors || Object.keys(errors).length === 0) return null;
return (
<ul className="error-list">
{Object.entries(errors).map(([key, params]) => {
const message = errorMessages[key as ErrorKey]?.(params) || 'Неверное значение';
return (
<li key={key} className="error-list__item">
{message}
</li>
);
})}
</ul>
);
}
// Использование
<TextField field={form.controls.password} label="Пароль" />;
{
password.touched && password.errors && <ErrorList errors={password.errors} />;
}
Иконки Ошибок
Визуальные индикаторы ошибок:
import { XCircle, CheckCircle, Loader } from 'lucide-react';
export function TextField({ field, label }: TextFieldProps) {
const control = useFormControl(field);
return (
<div className="text-field">
<label>{label}</label>
<div className="text-field__input-wrapper">
<input
value={control.value ?? ''}
onChange={(e) => control.setValue(e.target.value)}
onBlur={() => control.markAsTouched()}
/>
<div className="text-field__icon">
{control.pending && <Loader className="spin" />}
{!control.pending && control.touched && control.valid && (
<CheckCircle className="success" />
)}
{!control.pending && control.touched && control.invalid && <XCircle className="error" />}
</div>
</div>
{control.touched && control.errors && <ErrorMessage errors={control.errors} />}
</div>
);
}
Ошибки во Всплывающих Подсказках
Показывайте ошибки во всплывающих подсказках:
import { Tooltip } from '@radix-ui/react-tooltip';
export function TextField({ field, label }: TextFieldProps) {
const control = useFormControl(field);
const showError = control.touched && control.invalid;
return (
<div className="text-field">
<label>{label}</label>
<Tooltip open={showError}>
<Tooltip.Trigger asChild>
<input
value={control.value ?? ''}
onChange={(e) => control.setValue(e.target.value)}
onBlur={() => control.markAsTouched()}
className={showError ? 'error' : ''}
/>
</Tooltip.Trigger>
{showError && control.errors && (
<Tooltip.Content className="tooltip-error">
{getErrorMessage(control.errors)}
</Tooltip.Content>
)}
</Tooltip>
</div>
);
}
Обработка Ошибок на Уровне Формы
Сводка Ошибок
Отображайте все ошибки формы вверху:
interface ErrorSummaryProps {
form: GroupNode<any>;
}
export function ErrorSummary({ form }: ErrorSummaryProps) {
const formErrors = useFormControl(form).errors;
if (!formErrors) return null;
const allErrors = collectAllErrors(form);
if (allErrors.length === 0) return null;
return (
<div className="error-summary" role="alert">
<h3>Пожалуйста, исправьте следующие ошибки:</h3>
<ul>
{allErrors.map((error, index) => (
<li key={index}>
<a href={`#field-${error.fieldName}`}>
{error.fieldLabel}: {error.message}
</a>
</li>
))}
</ul>
</div>
);
}
// Вспомогательная функция для сбора всех ошибок
function collectAllErrors(form: GroupNode<any>) {
const errors: Array<{
fieldName: string;
fieldLabel: string;
message: string;
}> = [];
// Рекурсивный сбор ошибок из всех полей
const collectFromNode = (node: any, path: string[] = []) => {
if (node.errors?.value) {
const fieldName = path.join('.');
const message = getErrorMessage(node.errors.value);
errors.push({
fieldName,
fieldLabel: path[path.length - 1],
message,
});
}
if (node.controls) {
Object.entries(node.controls).forEach(([key, child]) => {
collectFromNode(child, [...path, key]);
});
}
};
collectFromNode(form);
return errors;
}
Toast Уведомления
Показывайте ошибки в виде toast уведомлений:
import { toast } from 'react-hot-toast';
export function FormWithToasts() {
const form = useMemo(() => createMyForm(), []);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
form.markAsTouched();
if (form.invalid.value) {
const errors = collectAllErrors(form);
errors.forEach((error) => {
toast.error(`${error.fieldLabel}: ${error.message}`);
});
return;
}
// Отправка формы
console.log('Валидно:', form.getValue());
};
return <form onSubmit={handleSubmit}>{/* поля */}</form>;
}
Модальное Окно с Ошибками
Показывайте ошибки в модальном окне:
import { Dialog } from '@radix-ui/react-dialog';
import { useState } from 'react';
export function FormWithErrorModal() {
const form = useMemo(() => createMyForm(), []);
const [showErrors, setShowErrors] = useState(false);
const [errors, setErrors] = useState<any[]>([]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
form.markAsTouched();
if (form.invalid.value) {
const allErrors = collectAllErrors(form);
setErrors(allErrors);
setShowErrors(true);
return;
}
// Отправка формы
};
return (
<>
<form onSubmit={handleSubmit}>{/* поля */}</form>
<Dialog open={showErrors} onOpenChange={setShowErrors}>
<Dialog.Content>
<Dialog.Title>Ошибки формы</Dialog.Title>
<Dialog.Description>Пожалуйста, исправьте следующие ошибки:</Dialog.Description>
<ul>
{errors.map((error, index) => (
<li key={index}>
<strong>{error.fieldLabel}:</strong> {error.message}
</li>
))}
</ul>
<Dialog.Close asChild>
<button>Закрыть</button>
</Dialog.Close>
</Dialog.Content>
</Dialog>
</>
);
}
Обработка Серверных Ошибок
Установка Серверных Ошибок
Обработка ошибок с сервера:
const form = useMemo(() => createRegistrationForm(), []);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
form.markAsTouched();
if (form.invalid.value) return;
try {
const response = await fetch('/api/register', {
method: 'POST',
body: JSON.stringify(form.getValue()),
});
if (!response.ok) {
const errors = await response.json();
// Установка серверных ошибок на полях
if (errors.username) {
form.controls.username.setErrors({ serverError: errors.username });
}
if (errors.email) {
form.controls.email.setErrors({ serverError: errors.email });
}
return;
}
// Успех
console.log('Зарегистрировано!');
} catch (error) {
console.error('Ошибка сети:', error);
}
};
Общая Серверная Ошибка
Показ общей серверной ошибки:
const [serverError, setServerError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setServerError(null);
form.markAsTouched();
if (form.invalid.value) return;
try {
const response = await fetch('/api/submit', {
method: 'POST',
body: JSON.stringify(form.getValue()),
});
if (!response.ok) {
const error = await response.json();
setServerError(error.message || 'Произошла ошибка');
return;
}
// Успех
} catch (error) {
setServerError('Ошибка сети. Пожалуйста, попробуйте снова.');
}
};
return (
<form onSubmit={handleSubmit}>
{serverError && (
<div className="alert alert-error" role="alert">
{serverError}
</div>
)}
{/* поля */}
</form>
);
Локализация Ошибок
Локализованные Сообщения об Ошибках
Поддержка нескольких языков:
utils/error-messages-i18n.ts
type Locale = 'ru' | 'en' | 'es';
const errorMessagesLocalized: Record<Locale, Record<ErrorKey, (params: any) => string>> = {
ru: {
required: () => 'Это поле обязательно',
email: () => 'Пожалуйста, введите корректный email',
minLength: (p) => `Минимум ${p.required} символов`,
// ...
},
en: {
required: () => 'This field is required',
email: () => 'Please enter a valid email',
minLength: (p) => `Must be at least ${p.required} characters`,
// ...
},
es: {
required: () => 'Este campo es obligatorio',
email: () => 'Por favor ingrese un email válido',
minLength: (p) => `Debe tener al menos ${p.required} caracteres`,
// ...
},
};
export function getLocalizedErrorMessage(
errors: Record<string, any>,
locale: Locale = 'ru'
): string {
const [key, params] = Object.entries(errors)[0];
const getMessage = errorMessagesLocalized[locale][key as ErrorKey];
return getMessage ? getMessage(params) : 'Неверное значение';
}
Использование с React Context
import { createContext, useContext } from 'react';
const LocaleContext = createContext<Locale>('ru');
export function ErrorMessage({ errors }: { errors: Record<string, any> }) {
const locale = useContext(LocaleContext);
const message = getLocalizedErrorMessage(errors, locale);
return <span className="error-message">{message}</span>;
}
// Обертка приложения
<LocaleContext.Provider value="en">
<MyForm />
</LocaleContext.Provider>;
Стилизация Ошибок
CSS Классы
Стилизация ошибок с помощью CSS:
/* Поле с ошибкой */
.input-error {
border-color: #dc2626;
background-color: #fef2f2;
}
.input-error:focus {
outline-color: #dc2626;
border-color: #dc2626;
}
/* Сообщение об ошибке */
.error-message {
color: #dc2626;
font-size: 0.875rem;
margin-top: 0.25rem;
display: flex;
align-items: center;
gap: 0.25rem;
}
/* Иконка ошибки */
.error-icon {
color: #dc2626;
width: 1rem;
height: 1rem;
}
/* Состояние успеха */
.input-success {
border-color: #16a34a;
}
.success-icon {
color: #16a34a;
}
/* Сводка ошибок */
.error-summary {
background-color: #fef2f2;
border: 1px solid #fecaca;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1.5rem;
}
.error-summary h3 {
color: #dc2626;
font-weight: 600;
margin-bottom: 0.5rem;
}
.error-summary ul {
list-style: disc;
padding-left: 1.5rem;
}
.error-summary a {
color: #dc2626;
text-decoration: underline;
}
Tailwind CSS
Используйте утилитарные классы Tailwind:
export function TextField({ field, label }: TextFieldProps) {
const control = useFormControl(field);
const showError = control.touched && control.invalid;
return (
<div className="space-y-1">
<label className="block text-sm font-medium text-gray-700">{label}</label>
<input
value={control.value ?? ''}
onChange={(e) => control.setValue(e.target.value)}
onBlur={() => control.markAsTouched()}
className={`
block w-full rounded-md px-3 py-2
focus:outline-none focus:ring-2
${
showError
? 'border-red-300 text-red-900 placeholder-red-300 focus:border-red-500 focus:ring-red-500'
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
}
`}
/>
{showError && control.errors && (
<p className="text-sm text-red-600 flex items-center gap-1">
<XCircle className="h-4 w-4" />
{getErrorMessage(control.errors)}
</p>
)}
</div>
);
}
Доступность
ARIA Атрибуты
Сделайте ошибки доступными:
export function TextField({ field, label }: TextFieldProps) {
const control = useFormControl(field);
const showError = control.touched && control.invalid;
const errorId = `${field.id}-error`;
return (
<div className="text-field">
<label htmlFor={field.id}>{label}</label>
<input
id={field.id}
value={control.value ?? ''}
onChange={(e) => control.setValue(e.target.value)}
onBlur={() => control.markAsTouched()}
aria-invalid={showError}
aria-describedby={showError ? errorId : undefined}
/>
{showError && control.errors && (
<span id={errorId} className="error-message" role="alert">
{getErrorMessage(control.errors)}
</span>
)}
</div>
);
}
Управление Фокусом
Автоматический фокус на первой ошибке:
import { useEffect, useRef } from 'react';
export function FormWithAutoFocus() {
const form = useMemo(() => createMyForm(), []);
const formRef = useRef<HTMLFormElement>(null);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
form.markAsTouched();
if (form.invalid.value) {
// Фокус на первой ошибке
setTimeout(() => {
const firstError = formRef.current?.querySelector('[aria-invalid="true"]') as HTMLElement;
firstError?.focus();
}, 0);
return;
}
// Отправка
};
return (
<form ref={formRef} onSubmit={handleSubmit}>
{/* поля */}
</form>
);
}
Объявления для Скринридеров
Объявляйте ошибки для скринридеров:
import { useEffect } from 'react';
export function FormWithAnnouncements() {
const form = useMemo(() => createMyForm(), []);
const [announcement, setAnnouncement] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
form.markAsTouched();
if (form.invalid.value) {
const errors = collectAllErrors(form);
setAnnouncement(
`В форме ${errors.length} ошибок${errors.length > 1 ? 'и' : 'а'}. Пожалуйста, исправьте их и попробуйте снова.`
);
return;
}
setAnnouncement('Форма успешно отправлена');
};
return (
<>
<div role="status" aria-live="polite" aria-atomic="true" className="sr-only">
{announcement}
</div>
<form onSubmit={handleSubmit}>{/* поля */}</form>
</>
);
}
Лучшие Практики
1. Показывайте Ошибки После Взаимодействия
// ✅ Хорошо - показывать после touched
{
control.touched && control.errors && <ErrorMessage errors={control.errors} />;
}
// ❌ Плохо - показывать сразу
{
control.errors && <ErrorMessage errors={control.errors} />;
}
2. Предоставляйте Полезные Сообщения
// ✅ Хорошо - конкретно и полезно
return {
passwordTooWeak: {
message:
'Пароль должен содержать заглавную букву, строчную букву, цифру и быть не менее 8 символов',
},
};
// ❌ Плохо - расплывчато
return { invalid: true };
3. Используйте Визуальные Индикаторы
// ✅ Хорошо - множественные индикаторы
<input className={showError ? 'error' : ''} aria-invalid={showError} />;
{
showError && <XCircle className="error-icon" />;
}
{
showError && <ErrorMessage />;
}
// ❌ Плохо - только текст
{
showError && <span>Ошибка</span>;
}
4. Обрабатывайте Серверные Ошибки Изящно
// ✅ Хорошо - устанавливать ошибки для конкретных полей
if (serverErrors.username) {
form.controls.username.setErrors({ serverError: serverErrors.username });
}
// ❌ Плохо - общий alert
alert('Ошибка сервера');
5. Делайте Ошибки Доступными
// ✅ Хорошо - ARIA атрибуты
<input
aria-invalid={showError}
aria-describedby={errorId}
/>
<span id={errorId} role="alert">
{errorMessage}
</span>
// ❌ Плохо - без доступности
<input className={showError ? 'error' : ''} />
<span>{errorMessage}</span>
Следующие Шаги
- Стратегии Валидации — Продвинутые паттерны валидации
- Кастомные Валидаторы — Создание кастомной логики валидации
- Композиция схем — Построение сложных форм