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
Читати статтю

Схожі послуги

Ці матеріали напряму пов’язані з послугами, які я реалізую в продакшені. Ознайомтесь із послугою або обговоріть свій випадок.