Что такое оптимистичный интерфейс — и почему лайк засчитывается раньше сервера

Смотри, забавная штука: когда ты ставишь лайк, сердечко краснеет мгновенно. А сервер об этом ещё ничего не знает. Запрос только вышел, ответ придёт через 200–800 миллисекунд — но ты уже видишь результат.
Интерфейс тебе соврал. И это осознанное решение, у него даже название есть — оптимистичный интерфейс.
Что это такое
Оптимистичный интерфейс — это когда приложение показывает результат действия сразу, не дожидаясь подтверждения от сервера. Оно исходит из того, что запрос почти наверняка пройдёт: сеть работает, прав хватает, база жива.
Обычный (пессимистичный) путь выглядит так:
- Ты нажал «Лайк».
- Кнопка ушла в спиннер.
- Сервер ответил
200 OK. - Сердечко покраснело.
Оптимистичный путь:
- Ты нажал «Лайк».
- Сердечко покраснело.
- Запрос ушёл в фоне.
- Если сервер ответил ошибкой — сердечко возвращается обратно.
Разница на глаз — это разница между «приложение тормозит» и «приложение отзывчивое». Хотя задержка сети в обоих случаях одинаковая. Поменялось только то, что происходит в эти сотни миллисекунд.
Где так делают, а где нельзя
Оптимистичный подход хорош, когда действие мелкое, частое и обратимое: лайк, галочка в списке дел, добавление в избранное, отправка сообщения в чате, перетаскивание карточки.
Признаки, что можно:
- Ты знаешь, как будет выглядеть результат, ещё до ответа сервера (сердечко красное — и всё).
- Ошибка маловероятна.
- Откат не страшен: вернуть сердечко в исходное — не катастрофа.
А вот где так делать не надо:
- Оплата. Нельзя показать «Оплачено», пока банк не ответил.
- Действия, результат которых придумывает сервер. Если после отправки формы сервер присваивает номер заказа — ты его не угадаешь.
- Всё, что ведёт к следующему шагу. Если человек увидел «Готово» и ушёл со страницы, ты уже не покажешь ему откат.
Вот это последнее — главная ловушка. Оптимистичный интерфейс не убирает риск, он перекладывает его на пользователя. Пока сервер молчит, человек уверен, что дело сделано, и действует дальше исходя из этого.
Как правильно откатывать
Работа в оптимистичном интерфейсе не в том, чтобы «показать сразу» — это одна строчка. Работа — в откате.
Плохой откат: значение молча вернулось назад. Человек в лучшем случае решит, что промахнулся мимо кнопки, и нажмёт ещё раз. В худшем — вообще не заметит, что лайк не сохранился.
Хороший откат делает три вещи:
- Возвращает состояние — сердечко снова серое.
- Говорит, что случилось — «Не удалось поставить лайк, нет сети».
- Даёт кнопку «Повторить» — потому что человек уже принял решение, не надо заставлять его принимать его заново.
Частный случай той же механики — отмена действия вместо «Вы уверены?»: там результат тоже показывают сразу, а страховку дают после.
Если сомневаешься, куда положить это сообщение — тост или подпись у самого элемента — посмотри тост или ошибка под полем: у них разные роли, и для откатов правило неочевидное.
Как это выглядит в коде
В React 19 для этого есть отдельный хук — useOptimistic. Он берёт настоящее значение и позволяет временно подменить его на ожидаемое:
const [optimisticLiked, setOptimisticLiked] = useOptimistic(liked);
function handleClick() {
startTransition(async () => {
setOptimisticLiked(true); // видно сразу
const saved = await saveLike();
setLiked(saved); // настоящее значение
});
}
Тут интересна одна деталь, которую легко пропустить. Ты нигде не пишешь «если ошибка — верни как было». Откат встроен: оптимистичное значение живёт ровно столько, сколько идёт действие. Если запрос упал, действие всё равно завершается, liked не поменялся — и интерфейс возвращается к нему сам. Причём без лишнего промежуточного кадра: оптимистичное и настоящее значения сходятся в одном рендере.
Из этого следует ограничение: setOptimisticLiked нужно вызывать внутри действия (startTransition или обработчика формы). Вызовешь снаружи — React предупредит в консоли, а значение мигнёт и тут же пропадёт.
Похожая механика есть и вне React: в TanStack Query это onMutate + onError, во Vue и Svelte — просто локальное состояние, которое ты сам возвращаешь назад в catch. Хук удобнее, идея одна и та же.
Что с этим делать прямо сейчас
Открой своё приложение и найди действие, после которого крутится спиннер дольше полусекунды. Спроси себя: я знаю результат заранее? Откат безболезненный? Если оба ответа «да» — покажи результат сразу, а запрос отправь в фоне.
И сразу, в том же коммите, напиши ветку ошибки. Не «потом» — потом её никто не пишет, и приложение начинает молча терять действия пользователей. Это, кстати, один из типичных пробелов в коде, который пишет ИИ: счастливый путь он делает отлично, а про catch забывает.
Оптимистичный интерфейс — это то же самое, что кэш?
Нет. Кэш показывает то, что уже было и подтверждено сервером. Оптимистичный интерфейс показывает то, чего ещё не было — предположение о будущем. Кэш может устареть, оптимистичное значение может оказаться неправдой.
А если человек закроет вкладку до ответа сервера?
Запрос, скорее всего, оборвётся. Поэтому для важных действий оптимистичный подход не годится. Обходной путь — отправлять такие запросы через navigator.sendBeacon или ставить их в очередь и повторять при следующем запуске, но это уже отдельная задача, а не «показать сердечко раньше».
Нужно ли это маленькому проекту?
Как правило, нет — начни с честного спиннера. Оптимистичный интерфейс имеет смысл там, где действие повторяется десятки раз за сессию: списки задач, чаты, ленты. Для формы, которую заполняют раз в жизни, выгода нулевая, а рисков заметно больше.
Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.





