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>
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ