JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Ограничения useOptimistic<...

Ограничения useOptimistic, best practices

Модуль 4: Node.js, Next.js и Angular
10 уровень , 8 лекция
Открыта

1. Ограничения useOptimistic

Оптимистичный UI — это как игра в угадайку: вы показываете пользователю результат его действия до того, как сервер реально его подтвердил. Если угадали — все счастливы. Если нет — приходится "откатывать" изменения и объяснять пользователю, что что-то пошло не так.

В Next.js 15 хук useOptimistic — мощный инструмент, но, как и любой power tool, требует аккуратности. Без понимания ограничений можно легко получить рассинхронизацию состояния, странные баги и даже потерю данных.

Только для Client Components

Первое и главное: useOptimistic работает только в Client Components. Если вы попытаетесь использовать его в Server Component, вас ждет ошибка и грустный жёлтый треугольник в консоли. Причина проста: оптимистичный UI — это чисто клиентская история, ведь только на клиенте мы можем мгновенно нарисовать "будущее".


// ❌ Не сработает — Server Component
// export default function MyServerComponent() {
//   const [optimisticData, addOptimistic] = useOptimistic(...);
// }

Не сохраняет состояние между переходами

useOptimistic не сохраняет оптимистичное состояние при переходе между страницами или при полной перерисовке компонента. Если пользователь ушёл со страницы и вернулся, optimistic-данные исчезнут. Это не баг, а фича: хук работает на уровне компонента, а не глобального состояния.

Нет автоматического отката при ошибке Server Action

Одна из самых частых ловушек: если Server Action вернул ошибку (например, сервер не добавил задачу из-за валидации), optimistic-данные не откатываются автоматически. Вам нужно самостоятельно реализовать логику возврата к прежнему состоянию или показать ошибку пользователю.


// Псевдокод: обработка ошибки и откат optimistic-UI
const action = async (formData) => {
  try {
    await addTaskOnServer(formData);
  } catch (e) {
    // Откатить optimistic-данные вручную
    setOptimisticData(prev => prev.filter(...));
    setError("Ошибка добавления задачи");
  }
};

Не подходит для сложных зависимых состояний

Если ваши optimistic-данные зависят от других частей состояния (например, счетчик задач, пагинация, фильтры), легко получить рассинхронизацию. Например, вы оптимистично добавили задачу, увеличили счетчик, а потом сервер вернул ошибку — и счетчик уже не совпадает с реальным количеством задач.

Нет глобального optimistic-стора

useOptimistic — локальный хук. Если вы хотите, чтобы optimistic-данные были видны в разных компонентах, придётся выносить логику выше по дереву, либо использовать глобальное состояние (например, через React Context или сторонние стейт-менеджеры). Сам по себе хук не волшебная палочка для всего приложения.

Не работает с синхронными действиями

useOptimistic задуман для асинхронных действий — в первую очередь для Server Actions. Если вы вызываете синхронную функцию, смысла в оптимистичном UI нет: данные и так обновятся мгновенно.

2. Типичные ошибки при использовании useOptimistic

Дублирование optimistic- и реальных данных

Частая ошибка: вы добавили optimistic-элемент в список, а когда сервер вернул настоящий объект — добавили его ещё раз. Получился дубль. Чтобы этого избежать, нужно синхронизировать optimistic-данные с реальными: например, заменить optimistic-элемент на настоящий по id или временной метке.


// Пример: добавление optimistic-задачи с временным id
addOptimistic(prev => [
  ...prev,
  { id: "temp-123", title: "Новая задача", optimistic: true }
]);

// Когда сервер вернул реальную задачу — заменить temp-элемент
setTasks(prev =>
  prev.map(task => task.id === "temp-123" ? realTaskFromServer : task)
);

Необработанные ошибки сервера

Пользователь видит optimistic-UI, но если сервер "ругается" (например, нет прав или ошибка валидации), пользователь не понимает, что произошло. Нужно обязательно показывать сообщение об ошибке и откатывать optimistic-изменения.

Отсутствие уникальных ключей для optimistic-элементов

Если вы добавляете optimistic-объекты в список, всегда используйте уникальный ключ (например, временный id). Иначе React может неправильно обновить DOM, и ваш optimistic-UI начнёт "плясать".

Несогласованность optimistic- и серверных данных

Если optimistic-UI отличается по структуре от реальных данных сервера (например, optimistic-объект без всех нужных полей), после синхронизации могут появиться баги. Старайтесь делать optimistic-объекты максимально похожими на настоящие.

3. Best practices при работе с useOptimistic

Добавляйте optimistic-данные в начало списка

Пользователь ожидает, что новая задача появится сразу сверху (или снизу, если у вас такой UX). Не заставляйте его скроллить в поисках "будущего".


addOptimistic(prev => [
  optimisticTask,
  ...prev
]);

Используйте временные id для optimistic-объектов

Придумайте уникальный идентификатор для каждого optimistic-элемента, чтобы потом легко заменить его на реальный объект с сервера.


const tempId = "temp-" + Date.now();

Показывайте индикаторы optimistic-статуса

Добавьте визуальный маркер (например, полупрозрачность или спиннер), чтобы пользователь видел: эта задача ещё не подтверждена сервером.


<li key={task.id} style={{ opacity: task.optimistic ? 0.5 : 1 }}>
  {task.title}
  {task.optimistic && <span>⏳</span>}
</li>

Откатывайте optimistic-UI при ошибке

Если сервер вернул ошибку, обязательно уберите optimistic-элемент и покажите пользователю причину.


if (serverError) {
  setOptimisticData(prev => prev.filter(t => t.id !== tempId));
  setError("Не удалось добавить задачу: " + serverError.message);
}

Синхронизируйте optimistic- и реальные данные

Когда сервер вернул настоящий объект, замените optimistic-элемент на реальный, а не добавляйте новый.


setTasks(prev =>
  prev.map(task => task.id === tempId ? realTask : task)
);

Используйте useOptimistic только для асинхронных действий

Не стоит городить optimistic-UI там, где можно просто дождаться ответа сервера за 50 мс. Используйте хук только для реально долгих операций (например, сеть, база данных, удалённый API).

Не используйте useOptimistic для глобального состояния

Если optimistic-данные нужны в разных частях приложения, выносите их в Context или используйте отдельный стейт-менеджер.

4. Практический пример: todo-list с useOptimistic

Давайте посмотрим на пример todo-листа, который мы развиваем по ходу курса. Добавим оптимистичное добавление задачи с временным id, индикатором статуса и обработкой ошибок.


"use client";
import { useState } from "react";
import { addTaskOnServer } from "../actions";
import { useOptimistic } from "react";

export default function TodoList({ initialTasks }) {
  const [tasks, setTasks] = useState(initialTasks);
  const [optimisticTasks, addOptimisticTask] = useOptimistic(tasks);
  const [error, setError] = useState(null);

  const handleAddTask = async (title) => {
    const tempId = "temp-" + Date.now();
    // 1. Добавляем optimistic-задачу
    addOptimisticTask(prev => [
      { id: tempId, title, optimistic: true },
      ...prev
    ]);

    try {
      // 2. Отправляем задачу на сервер
      const realTask = await addTaskOnServer({ title });
      // 3. Заменяем temp-задачу на реальную
      setTasks(prev =>
        prev.map(task => task.id === tempId ? realTask : task)
      );
    } catch (e) {
      // 4. Откатываем optimistic-UI и показываем ошибку
      setTasks(prev => prev.filter(task => task.id !== tempId));
      setError("Ошибка добавления задачи: " + e.message);
    }
  };

  return (
    <div>
      <form onSubmit={e => {
        e.preventDefault();
        handleAddTask(e.target.title.value);
        e.target.reset();
      }}>
        <input name="title" placeholder="Новая задача" />
        <button type="submit">Добавить</button>
      </form>
      {error && <div style={{ color: "red" }}>{error}</div>}
      <ul>
        {optimisticTasks.map(task => (
          <li key={task.id} style={{ opacity: task.optimistic ? 0.5 : 1 }}>
            {task.title}
            {task.optimistic && <span>⏳</span>}
          </li>
        ))}
      </ul>
    </div>
  );
}
</code></pre>

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ