ГлавнаяБлогuseOptimistic в React 19: мгновенный интерфейс без ручного отката
Алгоритмы

useOptimistic в React 19: мгновенный интерфейс без ручного отката

Разбираем useOptimistic в React 19: как мгновенно обновлять интерфейс, сочетать с useActionState и избегать типичных ошибок. Практические примеры внутри.

Al
Редакция Algolitalgolit.ru
7 мин чтения5 августа 2026 г.

Вы когда-нибудь нажимали «лайк», и интерфейс обновлялся с задержкой? Или комментарий появлялся только после ответа сервера? В этой статье вы научитесь делать мгновенные обновления с помощью useOptimistic — хука React 19, который избавляет от ручного отката состояния. Разберем реальные ошибки и их решения.

Проблема: ожидание сервера убивает UX

В первой части мы разобрали useActionState для ожидания ответа сервера. Но некоторые действия не должны ждать: лайки, комментарии, чекбоксы. Пользователь хочет видеть результат мгновенно, а не после сетевого запроса. Старый подход — оптимистичное обновление вручную:

function handleLike() {
  setLiked(true); // обновляем сразу
  fetch('/api/like', { method: 'POST' })
    .catch(() => setLiked(false)); // откат при ошибке
}

Это работает, пока не случается сбой: забытый catch оставляет ложное состояние, а повторные клики гоняют несколько запросов без гарантии порядка. useOptimistic решает эти проблемы, автоматически управляя временным состоянием.

Как работает useOptimistic

Сигнатура хука:

const [optimisticState, setOptimistic] = useOptimistic(value, reducer?);

value — реальное подтвержденное состояние. optimisticState совпадает с ним, пока вы не вызовете setter внутри Transition. Тогда оно временно отражает переданные данные. После завершения Transition optimisticState автоматически возвращается к value. Никаких ручных сбросов — это главное преимущество.

Комбинируем с useActionState: форма комментария

Вот пример, который объединяет оба хука. Здесь мы создаем комментарий с оптимистичным добавлением:

import { useActionState, useOptimistic, startTransition } from 'react';

function CommentBox({ comments, onConfirmed }) {
  async function postComment(previousState, formData) {
    if (formData === null) return { error: null };
    const text = formData.get('comment');
    if (!text?.trim()) return { error: 'Комментарий не может быть пустым.' };
    const saved = await saveComment(text);
    startTransition(() => onConfirmed({ id: saved.id, text }));
    return { error: null };
  }

  const [state, formAction, isPending] = useActionState(postComment, { error: null });
  const [optimisticComments, addOptimisticComment] = useOptimistic(comments);

  async function handleSubmit(formData) {
    const text = formData.get('comment');
    const id = crypto.randomUUID();
    addOptimisticComment((current) => [...current, { id, text }]);
    return formAction(formData);
  }

  function handleReset() {
    startTransition(() => formAction(null));
  }

  return (
    <div>
      <ul>
        {optimisticComments.map((c) => (
          <li key={c.id}>{c.text}</li>
        ))}
      </ul>
      <form action={handleSubmit}>
        <input name="comment" disabled={isPending} />
        <button disabled={isPending}>Отправить</button>
        <button type="button" onClick={handleReset} disabled={isPending}>Сбросить</button>
        {state.error && <p role="alert">{state.error}</p>}
      </form>
    </div>
  );
}

Важно: useOptimistic не обновляет реальные comments. Это делает родительский компонент через onConfirmed:

function CommentThread({ postId }) {
  const [comments, setComments] = useState(initialComments);
  return (
    <CommentBox
      comments={comments}
      onConfirmed={(comment) =>
        setComments((current) => [...current, comment])
      }
    />
  );
}

Если забыть эту связку, комментарий исчезнет после завершения Transition, даже если сервер его сохранил. Это ошибка №1, которую я совершил.

Ошибка с генерацией ID внутри обновления

Обратите внимание: id генерируется в handleSubmit через crypto.randomUUID(), а не внутри функции обновления для addOptimisticComment. Обновляющая функция должна быть чистой (pure). Если вызвать Date.now() или randomUUID внутри неё, React может вызвать её несколько раз, и вы получите разные ключи для одного комментария.

Сброс состояния: почему ремаунт опасен

useActionState не имеет автоматического сброса. Часто советуют менять key, чтобы пересоздать компонент. Это работает, но с useOptimistic появляется проблема: ремаунт уничтожает и оптимистичное состояние. Если Transition ещё не завершился, оптимистичный элемент просто исчезнет, не дождавшись реального.

Моя первая попытка — добавить сброс в редьюсер:

function commentReducer(current, action) {
  if (action.type === 'reset') return [];
  return [...current, action.item];
}

Это неправильно: current содержит весь список, включая подтвержденные комментарии. Возврат [] временно сотрет все, что уже сохранено. Пользователь увидит пустой список без причины.

Правильное решение — не сбрасывать useOptimistic вообще. Когда кнопка «Сбросить» доступна, isPending равно false, значит optimisticComments уже совпадает с comments. Сбрасывать нечего. Единственное, что нужно сбросить — ошибку в useActionState, что и делает formAction(null).

Отмена действия: AbortController и когда он не нужен

Вопрос: что если нажать «Сбросить» во время отправки? В черновике я ошибочно сказал, что React не поддерживает отмену. На самом деле есть документированный паттерн с AbortController.

Но! Отмена действия небезопасна: если сервер уже выполнил мутацию, отмена запроса не откатит её. Поэтому в handleReset мы не используем AbortController — вызов formAction(null) просто встает в очередь после текущего действия. Кнопка «Сбросить» отключена при isPending, чтобы пользователь не ждал зря.

Используйте AbortController только там, где критична отмена: например, поиск по мере ввода, где поздний ответ может запутать. Для мутаций безопаснее дать действию завершиться.

Вывод: доверяйте автоматике

useOptimistic берет на себя откат состояния. Мои ошибки были из-за недоверия к этому: я забыл связать onConfirmed, из-за чего не было реального состояния для возврата, и добавил лишний сброс, который ломал список. Доверяйте автоматическим частям, подключайте то, что не автоматизировано, и сбрасывайте только то, что действительно требует сброса.

Попробуйте прямо сейчас: возьмите свой компонент с лайком или комментарием и перепишите его с useOptimistic. Убедитесь, что onConfirmed обновляет реальное состояние, и не добавляйте лишних сбросов. Вы почувствуете разницу в отзывчивости интерфейса.

#useOptimistic#React 19#оптимистичные обновления#useActionState
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →