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»
