Оптимистичное обновление UI
Оптимистичное обновление (optimistic update) — паттерн, при котором UI обновляется немедленно до получения ответа от сервера, а при ошибке откатывается к предыдущему состоянию.
Зачем нужно
Классическое взаимодействие: пользователь нажимает «Лайк» → спиннер → ответ сервера → обновление иконки. Это 200-500 мс задержки на каждое действие — раздражает. Оптимистичное обновление убирает это ожидание: иконка меняется мгновенно, запрос к серверу уходит в фоне. При успехе — всё хорошо. При ошибке — откат. Twitter, GitHub, Notion используют этот паттерн повсеместно.
Где используется
- Лайки, звёзды, закладки — мгновенная обратная связь
- Удаление элементов из списка — элемент исчезает сразу
- Чекбоксы и переключатели — без задержки
- Редактирование inline — изменения видны до сохранения
Реализация без библиотеки
function LikeButton({ postId, initialLiked, initialCount }) {
const [liked, setLiked] = useState(initialLiked);
const [count, setCount] = useState(initialCount);
const handleLike = async () => {
// 1. Сохраняем предыдущее состояние для отката
const prevLiked = liked;
const prevCount = count;
// 2. Оптимистично обновляем UI немедленно
setLiked(!liked);
setCount(prev => liked ? prev - 1 : prev + 1);
try {
// 3. Отправляем запрос к серверу в фоне
await toggleLike(postId);
// Успех — ничего не делаем, UI уже обновлён
} catch (error) {
// 4. Ошибка — откатываем к предыдущему состоянию
setLiked(prevLiked);
setCount(prevCount);
// Опционально: показываем уведомление об ошибке
showToast('Не удалось сохранить. Попробуйте ещё раз.');
}
};
return (
<button onClick={handleLike} aria-pressed={liked}>
{liked ? '❤️' : '🤍'} {count}
</button>
);
}
React Query: optimistic updates
import { useMutation, useQueryClient } from '@tanstack/react-query';
function TodoItem({ todo }) {
const queryClient = useQueryClient;
const toggleMutation = useMutation({
mutationFn: (todoId) => toggleTodo(todoId),
// Вызывается ДО отправки запроса
onMutate: async (todoId) => {
// Отменяем текущие запросы (не затираем оптимистичный апдейт)
await queryClient.cancelQueries({ queryKey: ['todos'] });
// Сохраняем предыдущее состояние кеша
const previousTodos = queryClient.getQueryData(['todos']);
// Оптимистично обновляем кеш
queryClient.setQueryData(['todos'], (oldTodos) =>
oldTodos.map(t =>
t.id === todoId ? { ...t, done: !t.done } : t
)
);
// Возвращаем контекст для onError
return { previousTodos };
},
// При ошибке — откат к сохранённому состоянию
onError: (error, todoId, context) => {
queryClient.setQueryData(['todos'], context.previousTodos);
},
// После успеха или ошибки — рефетч для синхронизации с сервером
onSettled: => {
queryClient.invalidateQueries({ queryKey: ['todos'] });
},
});
return (
<li>
<input
type="checkbox"
checked={todo.done}
onChange={ => toggleMutation.mutate(todo.id)}
/>
{todo.title}
</li>
);
}
Частые ошибки
- Нет отката при ошибке — UI показывает неверное состояние; пользователь думает, что лайк поставлен, но он не сохранился.
- Оптимистичное обновление для деструктивных операций — удаление файла, перевод денег — требуют подтверждения, а не мгновенного выполнения.
- Нет индикации для ошибок — откат молча происходит, пользователь не понимает что произошло; добавляйте toast-уведомления.