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 19ref- обычный проп функционального компонента.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 Team. «React 19 Release Notes»
- React Team. «useOptimistic Reference»
- React Team. «use Reference»
- React Team. «useActionState Reference»
- React Team. «useFormStatus Reference»
- React Team. «Server Actions»
- Next.js Team. «Server Actions and Mutations»
- React Team. «React 19 Upgrade Guide»
- RFC 7636. «PKCE»
- HTTP Archive. «Web Almanac 2025 - JavaScript»
