React
useOptimistic и useFormStatus
Пользователь ставит лайк. Сердечко загорается мгновенно - хотя запрос к серверу ещё в полёте. Если сервер подтвердит, всё хорошо; если упадёт, сердечко гаснет обратно. Этот трюк называется оптимистичный UI, и раньше его собирали вручную: запомнить старое значение, поставить новое, на ошибке откатить, не забыть про гонки. React 19 дал хук useOptimistic, который ведёт этот танец сам, и useFormStatus, чтобы кнопка отправки знала, что форма прямо сейчас в процессе.
- React 19 (декабрь 2024): useOptimistic и useFormStatus стабилизированы как часть набора хуков для форм и действий
- Twitter/X, Instagram: лайки и подписки загораются мгновенно, а сетевое подтверждение приходит следом - классический оптимистичный UI
- Linear, Notion: изменения в списках и досках применяются немедленно, ощущение мгновенного отклика держится на оптимистичных апдейтах
- Next.js App Router: useOptimistic поверх Server Functions - дефолтный рецепт отзывчивых мутаций без ожидания раунд-трипа
- React Docs: документация описывает useOptimistic (показ и сверка) и useFormStatus (чтение pending у родительской формы)
Предварительные знания
- Actions и useActionState: проп action у формы, isPending, возврат ошибок
- Server Functions: мутации на сервере, вызываемые из клиента
- Понимание состояния и ре-рендера: как обновление состояния перерисовывает компонент
Откуда взялся оптимистичный UI
Идея 'показать результат до подтверждения сервером' старше React: так работали ещё ранние чаты и почтовые клиенты, чтобы скрыть задержку сети. Сообщение появлялось в ленте сразу, а доставка подтверждалась галочкой позже. Во фронтенде это годами делали руками: хранили предыдущее значение, оптимистично обновляли состояние, на ошибке откатывали, отдельно боролись с гонками, когда параллельных действий несколько. React 19 формализовал паттерн в хук useOptimistic. А отдельная боль форм - 'как кнопке узнать, что форма отправляется, не прокидывая флаг через десять компонентов' - решена хуком useFormStatus, который читает состояние ближайшей родительской формы напрямую.
useOptimistic: показать до подтверждения
useOptimistic принимает текущее реальное состояние и функцию обновления, а возвращает кортеж: оптимистичное значение и функцию, которая временно применяет к нему изменение. Пока серверная мутация в полёте, компонент рендерит оптимистичное значение, и пользователь видит результат немедленно. Сигнатура: const [optimistic, addOptimistic] = useOptimistic(state, (current, value) => next).
Порядок внутри action важен: сначала addOptimistic применяет временное изменение, и UI обновляется мгновенно, и только затем await ждёт серверную мутацию. Между этими двумя строками пользователь уже видит результат, хотя на сервере ещё ничего не подтвердилось. Поле sending: true позволяет визуально отметить запись как 'в процессе' - например, приглушить её.
Оптимистичное состояние существует только во время выполнения действия. Это не замена useState или серверного состояния, а наложенный поверх них временный слой. Когда действие завершится и реальное состояние обновится, оптимистичный слой пропадает сам собой.
В какой момент относительно серверной мутации показывается оптимистичное значение?
Сверка и откат
Самое ценное в useOptimistic - автоматическая сверка. Оптимистичное значение живёт лишь как наложение поверх базового состояния. Когда действие завершается и базовое состояние обновляется (сервер вернул свежие данные или произошла инвалидация), React отбрасывает оптимистичный слой и показывает реальное состояние. Если же действие упало, базовое состояние не изменилось, оптимистичный слой просто исчезает - и UI откатывается к тому, что было.
- Успех — addOptimistic показал запись сразу. Мутация прошла, серверное состояние обновилось, оптимистичный слой снят, в UI остаётся реальная запись с сервера.
- Ошибка — addOptimistic показал запись сразу. Мутация упала, базовое состояние не изменилось, оптимистичный слой снят, запись исчезает - автоматический откат без ручного кода.
Раньше тот же эффект писали руками: сохранить копию прежнего состояния, оптимистично его изменить, в catch вернуть копию обратно, и отдельно решать, что делать с несколькими параллельными действиями. useOptimistic убирает этот ручной учёт: разработчик описывает только как применить оптимистичное изменение, а откат и сверку с реальностью берёт на себя React.
useOptimistic уместен там, где успех вероятен и задержка сети заметна: лайки, отметки прочтения, добавление в список. Для критичных операций, где ошибка дорога (платёж, удаление), оптимистичный показ может ввести в заблуждение - там честнее дождаться подтверждения сервера.
Что произойдёт с оптимистичным обновлением, если серверная мутация завершится ошибкой?
useFormStatus: состояние родительской формы
useFormStatus отвечает на узкий, но частый вопрос: как вложенному элементу (обычно кнопке отправки) узнать, что родительская форма прямо сейчас отправляется. Хук читает состояние ближайшей формы выше по дереву и возвращает объект с полями pending, data, method, action. Никакого пропа через всё дерево прокидывать не нужно - кнопка спрашивает форму напрямую.
Ключевое ограничение: useFormStatus читает форму, внутри которой отрендерен сам компонент. Поэтому кнопку выносят в отдельный компонент SubmitButton и кладут внутрь form. Если вызвать хук в том же компоненте, где объявлена form, он не увидит её - он смотрит вверх по дереву, а не на форму, которую возвращает текущий компонент.
useFormStatus импортируется из react-dom, а не из react, и возвращает данные только когда вызван внутри потомка form. Вне формы pending всегда будет false. Это частая причина 'кнопка не реагирует на отправку': компонент с хуком оказался не внутри той самой формы.
Разница с isPending из useActionState: isPending живёт в компоненте, который вызвал useActionState и держит саму форму, а useFormStatus позволяет любому вложенному компоненту узнать pending без проброса пропов. Для переиспользуемой кнопки отправки удобнее useFormStatus, для логики самой формы - isPending.
Почему компонент с useFormStatus должен находиться внутри элемента form?
Связь с другими темами
Эти два хука завершают тему форм и мутаций. Что рядом:
- Actions и useActionState — Базовая обвязка формы: action, состояние, isPending; оба сегодняшних хука работают поверх неё
- Server Functions — Серверная мутация, которая подтверждает или отклоняет оптимистичное обновление
- Next.js App Router — Фреймворк, где связка оптимистичный UI + Server Function - стандарт отзывчивых интерфейсов
Итог
- useOptimistic(state, updateFn) возвращает оптимистичное значение и функцию для его временного применения до ответа сервера
- Оптимистичное значение показывается мгновенно, а когда базовое состояние обновится после мутации, React сам сверится и отбросит временное
- Если мутация упала, оптимистичное обновление откатывается автоматически - ручной откат и хранение прежнего значения больше не нужны
- useFormStatus читает состояние ближайшей родительской формы (pending, data, method) - флаг не нужно прокидывать пропами
- useFormStatus вызывается ТОЛЬКО внутри компонента, отрендеренного внутри form; типичное применение - кнопка отправки в отдельном компоненте
Связанные уроки
- rc-32-actions-useactionstate — Оба хука достраивают форму на Actions: useOptimistic применяется внутри action, useFormStatus читает её pending
- rc-31-server-functions — Оптимистичное обновление показывается мгновенно, а подтверждает его именно серверная мутация (Server Function)
- rc-35-nextjs-app-router — В App Router оба хука - стандартная связка для отзывчивых форм поверх серверных действий