Оптимистичное обновление 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-уведомления.

Связанные темы

Ресурсы