1. Введение
Когда пользователь кликает на кнопку "Добавить", "Удалить" или "Обновить" — он ожидает, что результат появится на экране сразу. Но на деле между кликом и реальным изменением данных может пройти несколько секунд: форма улетела на сервер, сервер подумал, сохранил, отправил ответ, клиент получил ответ, обновил список... И только тогда пользователь увидит результат.
Если просто ждать ответа сервера, интерфейс может "зависать", пользователь начинает кликать повторно, нервничать, а в худшем случае — думать, что сайт сломался. Чтобы этого не происходило, используется оптимистичный UI: мы показываем на экране ожидаемый результат сразу, ещё до того, как сервер подтвердил операцию. Если всё прошло хорошо — пользователь даже не замечает задержки. Если что-то пошло не так — мы "откатываем" изменения и показываем ошибку.
В Next.js 15 для этого есть удобный инструмент — хук useOptimistic. Он помогает реализовать локальное "оптимистичное" состояние для списков, форм и любых других данных.
Как работает useOptimistic: концепция
useOptimistic — это специальный хук для Client Components, который позволяет временно обновить UI так, будто операция уже завершилась успешно, а потом синхронизировать его с реальным результатом от сервера.
Принцип работы:
- Пользователь отправляет действие (например, добавляет задачу).
- Вы сразу обновляете локальное состояние UI, добавляя новую задачу в список — как будто сервер уже подтвердил операцию.
- Когда серверный Action завершился и вернул новый список/статус — вы синхронизируете UI с реальным состоянием.
- Если сервер вернул ошибку — откатываете оптимистичное изменение и показываете ошибку.
Аналогия:
Представьте, что вы отправляете письмо по почте и сразу записываете себе в блокнот "Письмо отправлено", хотя оно ещё не долетело до адресата. Если всё хорошо — всё совпало. Если письмо вернулось с пометкой "адресат не найден" — вы стираете запись и пишете "ошибка".
Базовый синтаксис 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} />;
}
Компонент Tasks — Client 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!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ