JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Оптимистичный UI: useOptim...

Оптимистичный UI: useOptimistic — концепция, примеры

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

1. Введение

Когда пользователь кликает на кнопку "Добавить", "Удалить" или "Обновить" — он ожидает, что результат появится на экране сразу. Но на деле между кликом и реальным изменением данных может пройти несколько секунд: форма улетела на сервер, сервер подумал, сохранил, отправил ответ, клиент получил ответ, обновил список... И только тогда пользователь увидит результат.

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

В Next.js 15 для этого есть удобный инструмент — хук useOptimistic. Он помогает реализовать локальное "оптимистичное" состояние для списков, форм и любых других данных.

Как работает useOptimistic: концепция

useOptimistic — это специальный хук для Client Components, который позволяет временно обновить UI так, будто операция уже завершилась успешно, а потом синхронизировать его с реальным результатом от сервера.

Принцип работы:

  1. Пользователь отправляет действие (например, добавляет задачу).
  2. Вы сразу обновляете локальное состояние UI, добавляя новую задачу в список — как будто сервер уже подтвердил операцию.
  3. Когда серверный Action завершился и вернул новый список/статус — вы синхронизируете UI с реальным состоянием.
  4. Если сервер вернул ошибку — откатываете оптимистичное изменение и показываете ошибку.

Аналогия:
Представьте, что вы отправляете письмо по почте и сразу записываете себе в блокнот "Письмо отправлено", хотя оно ещё не долетело до адресата. Если всё хорошо — всё совпало. Если письмо вернулось с пометкой "адресат не найден" — вы стираете запись и пишете "ошибка".

Базовый синтаксис useOptimistic


const [optimisticState, addOptimistic] = useOptimistic(
  state, // текущее (реальное) состояние
  (currentState, optimisticValue) => {
    // функция для вычисления нового состояния на основе "оптимистичного" действия
    return обновлённое_состояние;
  }
);
  • optimisticState — локальное состояние, которое будет меняться оптимистично.
  • addOptimistic — функция, которую нужно вызвать для применения оптимистичного обновления (например, при отправке формы).
  • state — текущее состояние (например, массив задач).
  • Второй аргумент — функция, описывающая, как применить оптимистичное изменение.

2. Пример: оптимистичное добавление задачи

Давайте рассмотрим самый популярный кейс — список задач (to-do), который мы вместе разрабатываем на протяжении курса.

Шаг 1. Исходные данные

Пусть у нас есть Server Component, который получает список задач с сервера и рендерит их:


// app/page.jsx (Server Component)
import Tasks from './Tasks';

export default async function Page() {
  const tasks = await fetchTasksFromDB();
  return <Tasks initialTasks={tasks} />;
}

Компонент TasksClient Component, который отображает список и форму добавления:


// app/Tasks.jsx
'use client';

import { useState } from 'react';

export default function Tasks({ initialTasks }) {
  const [tasks, setTasks] = useState(initialTasks);

  async function handleAdd(formData) {
    const newTask = await addTaskOnServer(formData.get('title'));
    setTasks([...tasks, newTask]);
  }

  return (
    <div>
      <form action={handleAdd}>
        <input name="title" />
        <button type="submit">Добавить</button>
      </form>
      <ul>
        {tasks.map(task => <li key={task.id}>{task.title}</li>)}
      </ul>
    </div>
  );
}

Проблема: После отправки формы задача появится в списке только после ответа сервера. Если сервер тормозит — пользователь ждёт.

Шаг 2. Добавляем useOptimistic

Теперь внедряем оптимистичный UI:


'use client';

import { useOptimistic } from 'react';
import { useFormState } from 'react-dom';

export default function Tasks({ initialTasks }) {
  // 1. useOptimistic: текущее состояние и функция для оптимистичного обновления
  const [optimisticTasks, addOptimisticTask] = useOptimistic(
    initialTasks,
    (currentTasks, newTaskTitle) => [
      ...currentTasks,
      { id: 'optimistic-' + Math.random(), title: newTaskTitle }
    ]
  );

  // 2. useFormState для Server Action
  const addTaskAction = async (prevState, formData) => {
    // Здесь вызывается Server Action (например, через fetch или серверную функцию)
    return await addTaskOnServer(formData.get('title'));
  };

  const [state, formAction] = useFormState(addTaskAction, null);

  return (
    <div>
      <form
        action={async (formData) => {
          // Сначала оптимистично обновляем UI
          addOptimisticTask(formData.get('title'));
          // Потом отправляем действие на сервер
          await formAction(formData);
        }}
      >
        <input name="title" />
        <button type="submit">Добавить</button>
      </form>
      <ul>
        {optimisticTasks.map(task => (
          <li key={task.id}>{task.title}</li>
        ))}
      </ul>
    </div>
  );
}

Объяснение:

  • useOptimistic хранит локальное состояние задач, которое обновляется мгновенно при добавлении.
  • addOptimisticTask вызывается при отправке формы, чтобы временно добавить задачу в список.
  • После завершения Server Action список синхронизируется с реальными данными (например, через обновление props или refetch).

Шаг 3. Улучшаем UX: показываем статус

Можно добавить визуальный индикатор, что задача "в ожидании подтверждения":


const [optimisticTasks, addOptimisticTask] = useOptimistic(
  initialTasks,
  (currentTasks, newTaskTitle) => [
    ...currentTasks,
    { id: 'optimistic-' + Math.random(), title: newTaskTitle, pending: true }
  ]
);

<ul>
  {optimisticTasks.map(task => (
    <li key={task.id}>
      {task.title}
      {task.pending && <span style={{ color: 'gray', marginLeft: 6 }}>(Добавляется...)</span>}
    </li>
  ))}
</ul>

Когда сервер вернёт обновлённый список, задача с pending: true исчезнет, а настоящая появится с реальным id.

3. Полезные нюансы

Оптимистичное удаление элемента

Оптимистичный UI работает не только для добавления, но и для удаления:


const [optimisticTasks, removeOptimisticTask] = useOptimistic(
  initialTasks,
  (currentTasks, deleteId) => currentTasks.filter(task => task.id !== deleteId)
);

function handleDelete(id) {
  // Сначала убираем задачу из UI
  removeOptimisticTask(id);
  // Потом отправляем запрос на сервер
  deleteTaskOnServer(id);
}

Пользователь сразу видит, что задача исчезла, даже если сервер ещё думает.

Важные нюансы и ограничения

  • Оптимистичное состояние — временное. После завершения Server Action вы должны синхронизировать UI с реальными данными (например, через refetch или обновление props).
  • id для новых элементов. Пока сервер не вернул настоящий id, используйте временный (например, строку с префиксом "optimistic-").
  • Обработка ошибок. Если сервер вернул ошибку — нужно "откатить" оптимистичное изменение и показать сообщение. Это можно сделать, например, через дополнительное состояние ошибки.
  • Конфликты. Если несколько пользователей одновременно изменяют один список, возможны конфликты. В большинстве случаев для простых задач это не критично, но для сложных сценариев потребуется дополнительная логика.

Когда использовать useOptimistic, а когда нет?

Используйте оптимистичный UI:

  • Если хотите мгновенно реагировать на действия пользователя (добавление, удаление, обновление).
  • Если задержки между действием и ответом сервера могут раздражать пользователя.
  • Для списков, форм, лайков, чекбоксов и т.п.

Не используйте:

  • Если операция критична и ошибка может привести к потере данных (например, удаление банковского счёта).
  • Если сервер может часто возвращать ошибки (тогда пользователь будет видеть "мигающий" UI).

Разница между useOptimistic и useFormState

  • useFormState — хранит состояние формы и результат Server Action (например, ошибки валидации, новый список задач).
  • useOptimistic — позволяет временно обновить UI, не дожидаясь ответа сервера.

Их можно и нужно использовать вместе: useOptimistic — для мгновенного отклика, useFormState — для синхронизации и обработки ошибок.

4. Типичные ошибки при работе с useOptimistic

Ошибка №1: забыли синхронизировать состояние после завершения Server Action.
В результате UI может навсегда остаться в "оптимистичном" состоянии, не показывая реальные данные с сервера.

Ошибка №2: не обрабатывается ошибка от сервера.
Если операция не удалась, а UI уже обновился — пользователь увидит "фантомные" элементы. Не забудьте откатить состояние и показать ошибку.

Ошибка №3: повторяющиеся id для оптимистичных и реальных элементов.
Если вы используете один и тот же id для оптимистичного и настоящего элемента, может возникнуть конфликт ключей в списке. Лучше использовать временный id для оптимистичного элемента.

Ошибка №4: изменение исходного массива напрямую.
В функции оптимистичного обновления обязательно возвращайте новый массив, не мутируя исходный. Иначе React не сможет корректно обновить UI.

Ошибка №5: попытка использовать useOptimistic в Server Component.
Этот хук работает только в Client Components!

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