useOptimistic это хук React 19, который позволяет экрану показать результат действия до того, как действие закончится. Вы устанавливаете оптимистичное значение внутри действия; React показывает его, пока действие ожидает завершения, а затем возвращается к настоящему состоянию. Если запрос не удался, настоящее состояние не менялось, поэтому экран откатывается сам.
Поддельный API в этом примере заменяет настоящий запрос и сбоит при каждом третьем вызове, чтобы вы увидели оба исхода.
Нажмите кнопку три раза, выжидая секунду между кликами. Первые два переключаются сразу, а строка о сохранении догоняет через секунду. Третий тоже переключается сразу, а затем возвращается обратно, когда запрос не удаётся. Замените calls % 3 на calls % 2, и сбоить будет каждый второй клик.
Синтаксис
const [optimisticState, setOptimistic] = useOptimistic(state, updateFn?);
stateэто настоящее значение, обычно изuseState, пропсов илиuseActionState. Когда никакое действие не ожидает,optimisticStateв точности равно ему.setOptimistic(value)устанавливает оптимистичное значение на всё время выполнения текущего действия.updateFnнеобязательна:(currentState, optimisticValue) => nextState. С нейsetOptimisticполучает изменение, а не целое новое значение. См. форму с редьюсером ниже.
Жизненный цикл одного клика всегда одинаков:
- Внутри действия вы вызываете
setOptimistic. Экран сразу обновляется. - Действие ждёт запрос.
- При успехе вы обновляете настоящее состояние (внутри
startTransition, если это происходит послеawait). - Действие заканчивается.
optimisticStateснова следует заstate: при успехе это сохранённое значение, при сбое старое.
Шаг 4 объясняет, почему в примере нет кода отката. Блок catch только устанавливает сообщение об ошибке.
Он должен выполняться внутри действия
Оптимистичное значение существует, только пока переход ожидает завершения, поэтому setOptimistic нужно вызывать внутри него. Подходит всё это:
- функция, которую вы передаёте в
startTransition(или вstartTransitionизuseTransition), - функция, переданная в
<form action={...}>или<button formAction={...}>, - действие, которое вы даёте useActionState.
При вызове из обычного onClick оптимистичному значению негде жить. React сразу его откатывает, а в разработке выводит "An optimistic state update occurred outside a transition or action".
Обновления настоящего состояния после await стоит оборачивать в собственный startTransition, как сделано выше в startTransition(() => setLiked(next)). После await React не может определить, что вы всё ещё внутри прежнего перехода.
Список сообщений с редьюсером
Когда оптимистичное состояние это список, передайте функцию обновления вторым аргументом. Тогда сеттер получает новый элемент, а React добавляет его к тому, каким настоящий список является в этот момент.
Отправьте три сообщения с разным текстом, по одному. Каждое появляется бледным с «(sending...)» и становится обычным после доставки; третье исчезает из чата и появляется как «Not sent». Отправьте их быстро, и все три останутся бледными, пока не закончится последний запрос, потому что React хранит оптимистичные значения, пока не завершатся все ожидающие действия. Действие формы уже является переходом, поэтому addOptimistic здесь не нужен собственный startTransition.
Давайте каждому оптимистичному элементу ключ, который не столкнётся с настоящими элементами. Пример использует pending- плюс текст, поэтому двойная отправка одного и того же текста, пока оба ожидают, привела бы к конфликту; настоящее приложение создавало бы id на клиенте.
Несколько обновлений сразу
Поскольку функция обновления получает текущее состояние, оптимистичные обновления складываются. Кликайте быстро, и каждый ожидающий клик применяется поверх предыдущего.
Быстро нажмите четыре раза. Корзина сразу покажет 4. Когда запросы закончатся, счётчик остановится на 3, потому что третий запрос не удался, и консоль об этом сообщает. React хранит оптимистичные значения, пока не завершатся все ожидающие действия, а затем показывает настоящее количество.
Когда использовать, а когда нет
Оптимистичные обновления подходят для действий, которые почти всегда успешны и легко отменяются: лайки, звёзды, переключатели, переименование, добавление сообщения. В обычном случае пользователь не видит индикатора загрузки.
Избегайте их там, где ложное «готово» введёт в заблуждение: платежи, удаление аккаунта, всё, на основе чего пользователь может действовать до того, как результат известен. Для таких случаев показывайте состояние ожидания через isPending из useActionState или useTransition и ждите настоящего ответа.
Всегда сообщайте пользователю, когда происходит откат. Значение, которое молча возвращается назад, выглядит как ошибка. Оба примера выше хранят сообщение об ошибке в обычном состоянии, чтобы оно пережило действие.
С useActionState
useOptimistic и useActionState хорошо сочетаются. Действие из useActionState уже является переходом, поэтому оптимистичное значение можно установить в его начале, а возвращённое состояние это настоящее значение, к которому вернётся оптимистичное.
const [state, formAction] = useActionState(async (previous, formData) => {
const title = formData.get('title');
setOptimisticTitle(title);
const saved = await saveTitle(title);
return { title: saved.title };
}, { title: 'Untitled' });
const [optimisticTitle, setOptimisticTitle] = useOptimistic(state.title);
Пока идёт сохранение, страница показывает новый заголовок. Когда оно завершится, state.title хранит сохранённое значение. Если saveTitle выбросит ошибку, ошибка действия уйдёт в ближайшую границу ошибок; чтобы вместо этого показать старый заголовок с сообщением, поймайте ошибку внутри действия и верните предыдущее состояние вместе с ошибкой.
Частые ошибки
Вызов сеттера вне действия. Оптимистичное значение не остаётся на экране, а в разработке React выводит предупреждение. Оберните вызов в startTransition или перенесите его в действие формы.
Забытое обновление настоящего состояния. При успехе действие должно изменить состояние, которое отражает useOptimistic (setLiked, setMessages). Иначе оптимистичное значение исчезнет, когда действие закончится, и успешный запрос будет выглядеть как откат.
Обновление настоящего состояния после await без перехода. Обновление всё равно применится, но React может показать его не в момент окончания действия. Оберните его в startTransition, как сделано во всех трёх примерах.
Оптимистичное значение как источник истины. Отправляйте запросы и считайте итоги по настоящему состоянию. Оптимистичное значение нужно только для отображения и может быть выброшено в любой момент.
Часто задаваемые вопросы
Что делает useOptimistic?
Он даёт копию части состояния, которую можно менять сразу, пока выполняется асинхронное действие. Когда действие заканчивается, копия снова следует за настоящим состоянием, в котором к тому моменту лежит либо сохранённый результат, либо старое значение.
Как useOptimistic откатывается при ошибке?
Особый путь для ошибок ему не нужен. Оптимистичное значение живёт только пока действие ожидает завершения. Если запрос не удался и вы так и не обновили настоящее состояние, хук снова показывает настоящее состояние, то есть значение до клика.
Почему я получаю "An optimistic state update occurred outside a transition or action"?
Сеттер был вызван из обычного обработчика события. Вызывайте его внутри функции, переданной в startTransition, внутри <form action> или внутри действия из useActionState.
Что за второй аргумент у useOptimistic?
Необязательная функция обновления, (currentState, optimisticValue) => newState, похожая на редьюсер. С ней сеттер получает только изменение (новое сообщение, +1), а React вычисляет оптимистичное состояние, даже когда одновременно ожидают несколько обновлений.
useOptimistic только для форм?
Нет. Он работает в любом переходе, поэтому его может использовать и кнопка, которая вызывает startTransition(async () => { ... }), и действие формы.