Skip to content

React 19: useOptimistic, use(), Server Actions - механизм и архитектура (2026)

React 19 - не инкрементальное обновление React 18. Он вводит новую модель мутаций - action-based архитектуру - где Server Actions, useOptimistic, useActionState и useFormStatus компонуются в полный паттерн серверной мутации, устраняя reducer + async middleware стек для большинства form-driven и data-mutation UI.

React 19 новые фичи: useOptimistic, use hook, Server Actions - архитектурная диаграмма
Автор:Опубликовано:Обновлено:Время чтения:16 мин

Senior Frontend Architect, 10+ лет опыта построения production-проектов на Next.js. Contentful Certified Professional (2024). Специализация: React Server Components, headless eCommerce, инженерия Core Web Vitals.

React 19, стабильный с декабря 2024 года, - самое крупное изменение поверхности API со времён хуков в 16.8. Он вводит пять новых примитивов, которые вместе сдвигают модель мутаций React от паттерна внешнего хранилища (Redux, Zustand, связка useState + useEffect + fetch) к модели на действиях: серверно осведомлённой, совместимой с прогрессивным улучшением и построенной поверх конкурентного рендерера React.

В этом посте я разбираю каждый примитив на уровне механизма:

  • useOptimistic - как композиция состояния в конкурентном рендере даёт мгновенную реакцию интерфейса с автоматическим откатом и без ручного управления состоянием
  • use() - почему чтение промисов и контекста внутри условий отличается от useContext и async/await и когда это действительно применимо
  • Server Actions - контракт RPC поверх HTTP, генерация идентификаторов действий, защита от CSRF и прогрессивное улучшение через атрибут action у формы
  • useActionState - паттерн протаскивания состояния, заменивший useFormState, и как isPending убивает ручной шаблон состояния загрузки
  • useFormStatus - механизм контекста родительской формы

В конце: как все пять складываются в единый паттерн мутации, который заменяет весь стек useState + useEffect + fetch + загрузка + откат.

Часть I - useOptimistic: композиция состояния в конкурентном рендере

useOptimistic - ответ React 19 на конкретную проблему UX, с которой сталкивается любой интерфейс с мутациями: разрыв между моментом, когда пользователь совершил действие (нажал «Нравится», отправил форму, добавил товар в корзину), и моментом, когда сервер это подтвердил. Без оптимистичного состояния интерфейс на каждой мутации замирает или показывает спиннер на 200–600 мс. С useOptimistic интерфейс сразу отражает действие пользователя, а если сервер его отклонит, React автоматически вернётся к подтверждённому состоянию.

  • Сигнатура: const [optimisticState, addOptimistic] = useOptimistic(state, updateFn). Первый аргумент - фактическое состояние, авторитетное со стороны сервера. Второй - функция обновления (currentState, optimisticValue) => newOptimisticState, чистая функция, которая вычисляет, как состояние должно выглядеть, если ожидающее действие пройдёт успешно. Вызов addOptimistic(value) немедленно применяет оптимистичное обновление.
  • Механизм композиции в фазе рендера: Когда вызывается addOptimistic(value), React не кладёт оптимистичное значение в отдельную переменную состояния - он ставит обновление в очередь на тот же слот, где живёт state. На следующем рендере React применяет updateFn(currentState, value) и получает optimisticState. Пока асинхронное действие в полёте (между вызовом addOptimistic и его завершением), каждый рендер возвращает составное оптимистичное состояние. Когда действие завершается и state обновляется ответом сервера, React выбрасывает оптимистичную композицию и рендерит с новым авторитетным состоянием. Если действие бросит исключение, React вернёт optimisticState к тому значению state, каким оно было до вызова addOptimistic: автоматический откат без единого обработчика ошибок.
  • Несколько одновременных оптимистичных обновлений: Если пользователь нажал «Нравится» на трёх постах до прихода любого ответа сервера, три вызова addOptimistic поставят в очередь три функции обновления. React применит их по порядку: updateFn(updateFn(updateFn(baseState, v1), v2), v3). У каждого ожидающего действия своя обработка ошибок: если упадёт второе, откатится только второе оптимистичное обновление, а первое и третье продолжат ждать или завершатся нормально.
  • Эквивалент до React 19 (для сравнения): Раньше оптимистичные обновления требовали такого: const [optimistic, setOptimistic] = useState(false); const [isPending, startTransition] = useTransition(); const handleClick = () => { setOptimistic(true); startTransition(async () => { try { await mutate(); } catch { setOptimistic(false); } }); }. useOptimistic заменяет весь этот паттерн одним вызовом хука и автоматическим откатом.

Часть II - Хук use(): условное чтение ресурсов

Функция use() названа так намеренно, а не usePromise или useContext: это общий примитив чтения ресурса. Она открывает механизм, который прежде существовал только во внутренностях Suspense, - прочитать ещё не готовый ресурс и приостановить компонент до его разрешения, причём с полной поддержкой условного вызова.

  • use(promise) - интеграция с Suspense в клиентских компонентах: const data = use(promise). Если промис ещё в полёте, React приостанавливает компонент - он бросает промис вверх к ближайшей границе Suspense, ровно как асинхронные функции RSC приостанавливаются на await. Когда промис разрешается, React возобновляет рендер с готовым значением. В отличие от связки useEffect + useState, промежуточного рендера с null не бывает: компонент рендерится ровно один раз и уже с данными. Это убирает защитный паттерн if (!data) return <Loading />.
  • use(promise) против асинхронных серверных компонентов: В серверном компоненте правильный паттерн - const data = await fetchData(): компонент асинхронный, и await останавливает его выполнение до готовности данных. use() нужен клиентским компонентам, которым надо прочитать промис, пришедший пропсом, обычно от серверного компонента, который этот запрос и начал. Серверный компонент стартует загрузку и передаёт промис клиентскому, а тот вызывает use(promise). Так получается загрузка, инициированная на сервере, с рендером на клиенте: запрос стартует на сервере, а граница Suspense живёт на клиенте.
  • use(context) - условное чтение контекста: const theme = use(ThemeContext). Функционально то же, что useContext(ThemeContext), но с одним критичным отличием: use() можно вызывать внутри условий и циклов. Обычные хуки после if появляться не могут - это правила хуков. use() не хук в традиционном смысле, а примитив компилятора, который не опирается на стабильность порядка вызовов. Это открывает конструкции вроде if (isAdmin) { const adminConfig = use(AdminContext); }, невозможные с useContext.
  • Контракт thenable: use() принимает любой thenable - объект с методом .then(), - а не только нативные промисы. Благодаря этому он совместим с собственными типами ресурсов, примитивами кеша (например, объектами Query из React Query) и лениво загружаемыми модулями.

Часть III - Server Actions: контракт RPC поверх HTTP

Server Actions - механизм, благодаря которому мутации в React 19 и App Router работают без ручного управления клиентскими эндпоинтами. Директива 'use server' на асинхронной функции превращает её из обычной функции в серверную операцию, доступную с клиента, без написания Route Handler или API-эндпоинта.

Механизм на уровне фреймворка: во время сборки Next.js каждой функции с 'use server' присваивается уникальный идентификатор действия - хеш от пути модуля и имени экспорта. Сервер держит реестр, сопоставляющий идентификаторы с реализациями функций.

Когда клиент вызывает Server Action, рантайм React сериализует аргументы по протоколу сериализации React DOM и отправляет HTTP POST на эндпоинт действий фреймворка, положив идентификатор в запрос. Сервер находит действие в реестре, десериализует аргументы и выполняет функцию. Ответ - потоковая нагрузка серверных компонентов, поэтому Server Action может запускать перерендер RSC и обновлять серверно отрисованное содержимое интерфейса.

  • Прогрессивное улучшение через <form action={serverAction}>: Когда Server Action передан атрибутом action элемента <form>, отправка формы работает без JavaScript: браузер делает обычный HTTP POST на эндпоинт действий, сервер обрабатывает действие, а ответом приходит полностью отрисованная страница. Когда JavaScript доступен, React перехватывает отправку и обрабатывает её потоковым обновлением RSC вместо полной перезагрузки. Это прогрессивное улучшение по умолчанию: один и тот же код работает и с включённым, и с выключенным JS.
  • Механизм защиты от CSRF: Реализация Server Actions в React генерирует токен запроса, привязанный к текущей сессии и источнику, и встраивает его в клиентский бандл как часть ссылки на действие. Токен проверяется на сервере до выполнения действия. Это предотвращает межсайтовую подделку запроса, не заставляя разработчика вручную возиться с CSRF-токенами. Защита работает автоматически и дополнительного middleware не требует.
  • Требование безопасности: все аргументы проверяются на сервере: Аргументы Server Action - это сериализованное тело HTTP POST, то есть данные, подконтрольные пользователю. Действие async function deleteItem(id: string) не должно верить, что id принадлежит текущему пользователю. Проверяйте: const user = await getSession(); const item = await db.items.findUnique({ where: { id, userId: user.id } }); if (!item) throw new Error('Unauthorized');. Никогда не считайте, что действие вызывается только из вашего интерфейса.
  • Размещение Server Actions в App Router: Действия можно объявить в файле actions.ts с 'use server' сверху, и тогда серверными становятся все экспорты, либо прямо внутри серверного компонента, поставив 'use server' первой строкой в теле функции. Встроенная форма допустима только в файлах серверных компонентов, но не в клиентских: те могут действия только вызывать, но не определять.

Часть IV - useActionState: протаскивание состояния для серверных мутаций

useActionState (переименован из useFormState в React 19 RC, сам useFormState объявлен устаревшим) связывает функцию-действие с постоянным значением состояния, протаскивая предыдущее состояние в каждый вызов. Это основной механизм обработки ответов Server Action в интерфейсе.

  • Сигнатура: const [state, dispatch, isPending] = useActionState(action, initialState, permalink?). У параметра action сигнатура (previousState: S, formData: FormData) => Promise<S>. На каждом вызове действие получает и предыдущее состояние, и новые данные формы. Возвращённое значение становится новым состоянием. dispatch вызывает действие или передаётся пропсом action у формы. isPending равен true, пока действие в полёте.
  • Паттерн протаскивания состояния: Действие - почти чистая функция, которая берёт текущее состояние и новый ввод и возвращает следующее состояние. Это зеркалит паттерн редьюсера из Redux и useReducer, но работает поверх асинхронных серверных операций. Пример: действие формы обратной связи возвращает с сервера { success: boolean, errors: Record<string, string> | null }; useActionState держит это состояние и отдаёт его обратно в действие при следующей отправке. Компонент читает state.errors и показывает сообщения валидации - без клиентского кода валидации и без useState под ошибки.
  • isPending как примитив состояния загрузки: Третье возвращаемое значение равно true с момента вызова dispatch до разрешения промиса действия. Это заменяет паттерн const [loading, setLoading] = useState(false): состоянием загрузки управляет сам React. Сочетание isPending с useOptimistic даёт полный паттерн «оптимистично плюс загрузка»: сразу показать оптимистичный интерфейс, показать индикатор во вторичных контролах и откатиться, если действие упало.
  • permalink для прогрессивного улучшения: Необязательный третий аргумент - URL, на который браузер перейдёт после завершения действия в сценарии без JavaScript. Это включает прогрессивное улучшение для страниц с useActionState: пользователи с выключенным JS завершают действие полным циклом страницы.

Часть V - useFormStatus: контекст родительской формы

useFormStatus читает статус отправки ближайшего вышестоящего элемента <form>. Главный сценарий - собирать кнопки отправки и компоненты обратной связи, которые реагируют на состояние отправки, не получая пропсов от контейнера формы.

  • Сигнатура: const { pending, data, method, action } = useFormStatus(). pending равен true, пока форма отправляется. data - отправляемый FormData (пригодится, чтобы показать превью отправленного). method - HTTP-метод. action - URL или функция действия.
  • Механизм контекста родительской формы: useFormStatus реализован через контекст React, который элементы <form> с пропом action автоматически предоставляют своему поддереву. Он читает состояние отправки ближайшей вышестоящей формы с действием, а не любой формы на странице. Поэтому useFormStatus обязан вызываться внутри компонента, который является потомком <form>: вызов в том же компоненте, который рисует <form>, вернёт { pending: false }, потому что контекст ещё не в области видимости.
  • Правильный паттерн <SubmitButton>: function SubmitButton() { const { pending } = useFormStatus(); return <button type='submit' disabled={pending}>{pending ? 'Отправляем...' : 'Отправить'}</button>; }. Такой компонент годится для любой формы: он сам читает состояние родителя без прокидывания пропсов. В прежних паттернах React того же добивались, передавая проп isLoading из контейнера формы в кнопку, и это связывало форму с её кнопкой отправки.

Часть VI - ref как проп и контекст как провайдер

React 19 приносит два синтаксических упрощения, которые сокращают шаблонный код, не меняя семантики. Оба обратно совместимы: старые API продолжают работать.

  • ref как проп (forwardRef устарел): В React 18 функциональный компонент не мог принять ref пропсом без обёртки React.forwardRef(). Обёртка была нужна, потому что React исторически перехватывал ref до того, как тот доходил до компонента. В React 19 ref - обычный проп функционального компонента. function Input({ ref, ...props }) { return <input ref={ref} {...props} />; } работает без forwardRef. Изменение живёт в согласователе: он больше не обрабатывает ref особым образом для функциональных компонентов, а пропускает его как любой другой проп. React.forwardRef продолжает работать, но в разработке пишет предупреждение об устаревании.
  • Контекст как провайдер (<Context value={...}>): В React 18 для контекста требовалось <ThemeContext.Provider value={theme}>. В React 19 сам объект контекста - валидный элемент JSX: <ThemeContext value={theme}>. Старый синтаксис <Context.Provider> продолжает работать. Реализация: createContext возвращает объект, который одновременно и потребитель, и провайдер, а форма JSX-элемента рендерит вариант провайдера. Это косметическое изменение без разницы в поведении.
  • Функции очистки из ref: Колбэки ref в React 19 могут возвращать функцию очистки, как useEffect. <div ref={(node) => { node.addEventListener('click', handler); return () => node.removeEventListener('click', handler); }}. React вызовет очистку при размонтировании компонента или при переназначении ref. Раньше механизма очистки у ref-колбэков не было: приходилось проверять if (node === null) в том же колбэке.

Часть VII - API метаданных документа и загрузки ресурсов

  • Подъём метаданных документа: В React 19 теги <title>, <meta> и <link>, отрисованные внутри компонентов, автоматически поднимаются в <head> документа. function ArticlePage({ title }) { return <article><title>{title}</title><p>Content</p></article>; } отрисует <title> в <head>, а <p> в <body>. Дубликаты <title> схлопываются, побеждает последний. Работает и в клиентских, и в серверных компонентах и не зависит от фреймворка. В App Router для SSR-метаданных по-прежнему предпочтителен generateMetadata(), потому что он дружит со стримингом и вычисляется до рендера страницы; API уровня компонента полезнее для клиентских SPA и приложений вне Next.js.
  • Дедупликация стилей через precedence: <link rel='stylesheet' href='/critical.css' precedence='high' />, отрисованный в любом компоненте, вставляет стиль в <head> с дедупликацией: несколько компонентов с одинаковым href дадут один тег <link>. Атрибут precedence задаёт порядок вставки: стили с 'high' идут раньше 'default'. Так получается загрузка CSS в области компонента без рантайма CSS-in-JS и без извлечения на сборке.
  • Функции предзагрузки ресурсов (react-dom): React 19 экспортирует императивные функции для подсказок ресурсов: import { preload, preloadModule, prefetchDNS, preconnect } from 'react-dom'. Вызов preload('/fonts/inter.woff2', { as: 'font', type: 'font/woff2', crossOrigin: 'anonymous' }) выдаёт <link rel='preload'> в head документа. Эти функции можно звать где угодно, включая обработчики событий, и они дедуплицируются. Они интегрированы со стриминговым рендерером, поэтому подсказки предзагрузки попадают в первый чанк HTML даже если вызваны из компонентов, которые отрисуются позже.

Часть VIII - Архитектура мутаций на действиях

Пять новых примитивов React 19 спроектированы, чтобы складываться друг с другом. Архитектурный синтез, который команда React называет моделью мутаций на действиях, заменяет паттерн useState + useEffect + fetch + ручное состояние загрузки + ручное состояние ошибки + ручной откат, бывший стандартом в React 18 для серверных мутаций. Понимать композицию нужно, чтобы решить, когда эту модель стоит принимать.

  • Паттерн композиции: Полный поток мутации в React 19 использует: (1) Server Action - серверную функцию, которая выполняет мутацию; (2) useActionState(serverAction, initialState) - связывает действие с постоянным состоянием и даёт isPending; (3) useOptimistic(serverState, updateFn) - даёт мгновенную реакцию, пока действие в полёте; (4) useFormStatus в компоненте кнопки - блокирует кнопку на время отправки. Эти четыре примитива заменяют собой хранилище Zustand с флагом загрузки, состояние ошибки, оптимистичное состояние, вызов fetch в useEffect и try/catch для отката.
  • Пример: добавление в корзину в headless Shopify: const [cartState, addToCart, isPending] = useActionState(addToCartAction, initialCart); const [optimisticCart, addOptimistic] = useOptimistic(cartState, (state, newLine) => ({ ...state, lines: [...state.lines, newLine] })); const handleAddToCart = async (variantId) => { addOptimistic({ variantId, quantity: 1 }); await addToCart({ variantId }); };. Счётчик корзины обновляется мгновенно по клику; если вызов API Shopify упадёт, корзина откатится; isPending включает состояние загрузки на кнопке. Это заменяет полную реализацию мутаций корзины из гайда по headless Shopify - та же идея, но нативным API React 19.
  • Когда модель действий НЕ заменяет внешние хранилища: Сложное клиентское состояние, не связанное с серверными мутациями - перетаскивание, состояние многошагового мастера, совместное редактирование в реальном времени поверх WebSocket, - по-прежнему живёт в useReducer, Zustand или XState. Модель действий выигрывает именно у серверных мутаций: отправок форм, записей в базу, изменений через API. Для состояния, которое целиком живёт на клиенте, прежние паттерны остаются уместными.
  • Интеграция с App Router: Server Actions в Next.js автоматически инвалидируют Full Route Cache и запускают перерендер RSC для затронутого маршрута по завершении. Действие, которое обновляет остатки товара, вызывает revalidateTag('inventory'), и страницы товаров перерисовываются со свежими остатками. Это делает Server Actions правильной точкой интеграции для мутаций в App Router: они одновременно и механизм мутации, и триггер инвалидации кеша. Архитектурный контекст - в гайде по миграции на App Router.

Часть IX - Ломающие изменения и миграция с React 18

  • useFormState переименован в useActionState: useFormState из react-dom в React 19 объявлен устаревшим. Замена - useActionState из react. Сигнатура почти та же: добавился isPending третьим возвращаемым значением, а сам хук переехал из react-dom в react.
  • ReactDOM.render и ReactDOM.hydrate удалены: Удалены в React 19, устаревшими считались с React 18. Замена - createRoot и hydrateRoot. Это касается унаследованных приложений на классовых компонентах, которые ещё не мигрировали.
  • Изменения поведения Strict Mode: Strict Mode в React 19 больше не вызывает дважды функции-инициализаторы состояния. Двойной вызов функций рендера и связки setup/cleanup у useEffect в разработке сохранился - тут ничего не изменилось.
  • Тайминг доступа к объекту ref: В React 18 обращение к ref.current во время рендера возвращало null на первых рендерах. React 19 делает это поведение предсказуемее и единообразнее в конкурентном режиме, но код, который полагался на конкретный момент заполнения ref.current, может повести себя иначе.
  • Изменения в TypeScript: React 19 везёт обновлённые определения типов, где ref - валидный проп функционального компонента (никакого forwardRef в типах), и добавляет типы для use(), useOptimistic, useActionState и useFormStatus. Обновляйте @types/react до 19.x вместе с самим пакетом.

Заключение

У новых примитивов React 19 понятная логика: мутация должна быть действием - функцией, которая берёт ввод и возвращает новое состояние, - а асинхронный жизненный цикл (ожидание, оптимистичные обновления, откат) берёт на себя фреймворк. Композиция useOptimistic + useActionState + useFormStatus + Server Actions - не новый слой абстракции поверх React, а конкурентный рендерер, который наконец вывел наружу примитивы, прежде доступные только через низкоуровневые внутренности Suspense и useTransition.

Для команд на App Router модель действий и Server Actions - естественная архитектура мутаций. Архитектура PPR хорошо ложится на useOptimistic для интерактивности динамических дыр. Меньшие клиентские бандлы от RSC вместе с исчезновением шаблонного кода мутаций делают React 19 очевидным апгрейдом для большинства продакшен-приложений на Next.js.

Ревью архитектуры на React 19 или сопровождение внедрения - услуга разработки React SPA | кейсы | обсудить проект.

Частые вопросы

  • useOptimistic заменяет все паттерны оптимистичных обновлений? Он заменяет связку useState + useTransition + ручной откат для обновлений, жёстко привязанных к одному асинхронному действию. Для сложного оптимистичного состояния, растянутого на несколько компонентов и действий - скажем, совместного редактора в реальном времени, - внешнее управление состоянием (Zustand, Jotai, XState) с явной логикой разрешения конфликтов остаётся уместным.
  • Можно ли использовать use(promise) в серверных компонентах? Нет. Серверные компоненты - асинхронные функции, там await напрямую. use() - примитив клиентских компонентов для чтения промисов, созданных на сервере и переданных пропсом. Типичный паттерн: серверный компонент создаёт промис, не дожидаясь его, передаёт клиентскому, а тот вызывает use(promise) и читает значение с интеграцией в Suspense.
  • Доступны ли Server Actions без Next.js? Server Actions - возможность React 19, включаемая директивой 'use server', но им нужен фреймворк, который реализует реестр эндпоинтов действий и стриминг RSC. Сейчас Next.js - основной продакшен-фреймворк, где это сделано. Серверные компоненты и действия в чистом Vite или Webpack требуют дополнительной настройки (react-server-dom-webpack).
  • Чем useActionState отличается от useReducer? useReducer - синхронная клиентская машина состояний: редьюсер выполняется синхронно и асинхронные операции делать не может. useActionState - запускатель асинхронных действий: функция действия асинхронна, выполняется на сервере или на клиенте, и её возвращаемое значение становится новым состоянием. По сути это асинхронная, серверно осведомлённая версия useReducer.
  • Стоит ли мигрировать с Redux на модель действий React 19? Для серверных мутаций - отправок форм, записей в базу, изменений через API - да: useActionState вместе с Server Actions строго проще и мощнее. Для сложного клиентского состояния приложения (история навигации, многошаговые сценарии интерфейса, состояние реального времени) Redux и Zustand остаются валидными. Миграция инкрементальна: начинайте с замены отдельных потоков мутаций, а не с переписывания всего слоя управления состоянием.

Источники

Похожие статьи

React Server Components: как на самом деле работает архитектура с нулевым бандлом (2026)

Глубокое погружение в React Server Components - модель рендеринга, которая убирает клиентский JavaScript для серверных поддеревьев. Wire-формат RSC, семантика границ, async-загрузка данных, Suspense-стриминг, Partial Prerendering и React 19 Compiler - с реальными бенчмарками и практическим фреймворком принятия решений.

ReactNext.jsArchitecture
Читать статью

Shopify Hydrogen vs Next.js Commerce: какую архитектуру выбрать для магазина в 2026

Детальное сравнение двух основных headless-архитектур для Shopify. Модель исполнения, слой данных, стратегия кэширования, размер бандла, бенчмарки производительности (LCP/INP/CLS), TCO, риск вендор-локина и практический фреймворк решений между нативной глубиной Hydrogen и компонуемой гибкостью Next.js Commerce.

eCommerceNext.jsShopify
Читать статью

Миграция на Next.js App Router с Pages Router: полный практический гайд (2026)

Практический гайд по миграции продакшн-приложений Next.js с Pages Router на App Router. Стоимостная модель гидратации RSC, механика модульного графа webpack и директивы 'use client', внутреннее устройство Data Cache, инкрементальная стратегия, разбор типичных ошибок и фреймворк принятия решений.

Next.jsArchitectureMigration
Читать статью

Похожие услуги

Эти материалы напрямую связаны с услугами, которые я реализую в продакшене. Изучите услугу или обсудите свой случай.