Как обработать ошибку при изменении данных?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Стратегия обработки ошибок при обновлении
При изменении данных на клиенте нужно обработать несколько слоев: валидация на уровне формы, отправка запроса на сервер, обработка ошибок сервера и откат изменений. Правильная обработка ошибок заключается в том, чтобы пользователь всегда знал, что произошло и что с этим делать.
Реализация с React Hooks и асинхронными операциями
import { useState } from 'react';
import { useCallback } from 'react';
interface UpdateUserPayload {
name: string;
email: string;
}
interface ApiError {
status: number;
message: string;
field?: string;
}
export function UserProfileEditor() {
const [data, setData] = useState({ name: 'John', email: 'john@example.com' });
const [errors, setErrors] = useState<Record<string, string>>({});
const [isLoading, setIsLoading] = useState(false);
const [successMessage, setSuccessMessage] = useState('');
const [originalData, setOriginalData] = useState(data);
const validateData = (payload: UpdateUserPayload): boolean => {
const newErrors: Record<string, string> = {};
if (!payload.name.trim()) {
newErrors.name = 'Имя не может быть пустым';
}
if (!payload.email.includes('@')) {
newErrors.email = 'Некорректный формат email';
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const handleUpdate = useCallback(async (payload: UpdateUserPayload) => {
setErrors({});
setSuccessMessage('');
if (!validateData(payload)) {
return;
}
setIsLoading(true);
try {
const response = await fetch('/api/v1/user/profile', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
if (!response.ok) {
const errorData: ApiError = await response.json();
if (response.status === 400) {
setErrors({
[errorData.field || 'general']: errorData.message
});
} else if (response.status === 409) {
setErrors({ email: 'Этот email уже зарегистрирован' });
} else if (response.status === 401) {
window.location.href = '/login';
return;
} else if (response.status === 500) {
setErrors({ general: 'Ошибка сервера. Попробуйте позже.' });
}
setData(originalData);
return;
}
const updatedData = await response.json();
setData(updatedData);
setOriginalData(updatedData);
setSuccessMessage('Данные успешно обновлены');
setTimeout(() => setSuccessMessage(''), 3000);
} catch (error) {
if (error instanceof Error) {
setErrors({ general: `Сетевая ошибка: ${error.message}` });
} else {
setErrors({ general: 'Неизвестная ошибка' });
}
setData(originalData);
} finally {
setIsLoading(false);
}
}, [originalData]);
const handleChange = (field: string, value: string) => {
setData(prev => ({ ...prev, [field]: value }));
setErrors(prev => ({ ...prev, [field]: '' }));
};
const handleCancel = () => {
setData(originalData);
setErrors({});
};
return (
<form onSubmit={(e) => { e.preventDefault(); handleUpdate(data); }}>
{errors.general && (
<div className="p-3 bg-red-100 text-red-700 rounded-lg mb-4">
{errors.general}
</div>
)}
{successMessage && (
<div className="p-3 bg-green-100 text-green-700 rounded-lg mb-4">
{successMessage}
</div>
)}
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Имя</label>
<input
type="text"
value={data.name}
onChange={(e) => handleChange('name', e.target.value)}
className={`w-full px-3 py-2 border rounded ${errors.name ? 'border-red-500' : 'border-gray-300'}`}
disabled={isLoading}
/>
{errors.name && <p className="text-red-500 text-sm mt-1">{errors.name}</p>}
</div>
<div className="mb-4">
<label className="block text-sm font-medium mb-1">Email</label>
<input
type="email"
value={data.email}
onChange={(e) => handleChange('email', e.target.value)}
className={`w-full px-3 py-2 border rounded ${errors.email ? 'border-red-500' : 'border-gray-300'}`}
disabled={isLoading}
/>
{errors.email && <p className="text-red-500 text-sm mt-1">{errors.email}</p>}
</div>
<div className="flex gap-2">
<button
type="submit"
disabled={isLoading}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{isLoading ? 'Сохранение...' : 'Сохранить'}
</button>
<button
type="button"
onClick={handleCancel}
disabled={isLoading}
className="px-4 py-2 border rounded hover:bg-gray-100 disabled:opacity-50"
>
Отменить
</button>
</div>
</form>
);
}
Типы ошибок и их обработка
Ошибки валидации (400): Показываем ошибки рядом с полями, чтобы пользователь сразу понял, что нужно исправить.
Конфликты данных (409): Возникают, когда данные конфликтуют с существующими (например, email занят). Откатываем состояние и показываем конкретное сообщение.
Сетевые ошибки: Обрабатываем в catch блоке. Показываем пользователю, что это техническая проблема, а не его ошибка.
Сессия истекла (401): Перенаправляем на логин вместо показания сообщения об ошибке.
Best Practices
Откат состояния: Всегда сохраняем исходные данные в originalData, чтобы откатить изменения при ошибке.
UX обратной связи: Показываем статус (загрузка, успех, ошибка) через состояния. Кнопка заблокирована во время загрузки, чтобы избежать дублированных запросов.
Таймаут сообщений: Сообщение об успехе автоматически исчезает, чтобы не загромождать интерфейс.