Комментарий пользователя
Здравствуйте! Хочу, чтобы интерфейс мгновенно показывал результат отправки (например, когда добавляю комментарий), а потом синхронизировал с сервером. Как реализовать обновление так, чтобы при ошибке откатывать состояние и не допустить рассинхронизации?
Ответ специалиста
Здравствуйте, Петр! Optimistic UI делает интерфейс быстрее и отзывчивее: сразу обновляет локальное состояние. Он показывает новый элемент, еще до ответа сервера. А если сервер потом вернет ошибку, интерфейс либо откатится назад, либо обновит данные реальными значениями от сервера.
В коде ниже показан импорт и вспомогательная функция API для имитации запроса — в реальном проекте их нужно заменить на fetch или axios:
import React, { useState, useCallback } from 'react';
// Симуляция POST /comments: задержка + 15% шанс ошибки
export async function postCommentApi(body) {
// В реальном коде: return fetch('/api/comments', { method: 'POST', body: JSON.stringify(body) }).then(r => r.json())
await new Promise(r => setTimeout(r, 700));
if (Math.random() < 0.15) throw new Error('Server error');
return { id: Date.now(), ...body }; // сервер вернет реальный id
}
Компонент списка комментариев. Временные комментарии нужно визуально выделять — например, снижать непрозрачность и показывать специальный текст, чтобы пользователь видел, что это временный элемент. Код должен быть внутри React-компонента:
import React, { useState, useCallback } from 'react';
import { postCommentApi } from './api'; // путь к блоку с API
import CommentForm from './CommentForm'; // путь к блоку с формой
export default function Comments() {
const [comments, setComments] = useState([]);
const [pending, setPending] = useState(false);
const addComment = useCallback(async (text) => {
const tempId = `tmp_${Date.now()}`;
const temp = { id: tempId, text, status: 'pending' };
// оптимистично показываем комментарий в UI
setComments(prev => [temp, ...prev]);
setPending(true);
try {
const saved = await postCommentApi({ text });
// заменяем временный элемент на реальный ответ от сервера
setComments(prev => prev.map(c => c.id === tempId ? { ...saved, status: 'saved' } : c));
} catch (err) {
// при ошибке откатываем — удаляем временный элемент
setComments(prev => prev.filter(c => c.id !== tempId));
console.error('Failed to save comment:', err);
// здесь можно показать toast/alert с ошибкой
} finally {
setPending(false);
}
}, []);
return (
<div>
<CommentForm onSubmit={addComment} disabled={pending} />
<ul>
{comments.map(c => (
<li key={c.id} style={{ opacity: c.status === 'pending' ? 0.6 : 1 }}>
{c.text} {c.status === 'pending' ? '(sending...)' : ''}
</li>
))}
</ul>
</div>
);
}
В компонент списка комментариев должен быть импортирован компонент формы CommentForm использует state для ввода:
import React, { useState } from 'react';
export default function CommentForm({ onSubmit, disabled }) {
const [text, setText] = useState('');
const handle = async (e) => {
e.preventDefault();
if (!text.trim()) return;
await onSubmit(text.trim());
setText('');
};
return (
<form onSubmit={handle}>
<input value={text} onChange={e => setText(e.target.value)} disabled={disabled} />
<button type="submit" disabled={disabled || !text.trim()}>Send</button>
</form>
);
}
Обратите внимание: добавление комментария должно происходить мгновенно. После этого либо появится сохраненный комментарий с реальным ID, либо временный исчезнет, если произошла ошибка, а пользователю покажется соответствующее сообщение.
Не создавайте ключи элементов прямо в render, например с помощью Math.random(). Временный ID нужно генерировать один раз при создании объекта, чтобы ключ оставался стабильным до окончания операции.
Больше полезных материалов и новости из мира IT — в рассылке Академии Selectel.