; React 19 · Новые хуки — интерактивный урок
Урок · React 19

Четыре хука, которые
убирают рутину

Раньше, чтобы отправить одну форму, приходилось заводить несколько отдельных переменных состояния: одну для данных, вторую для флага загрузки, третью для ошибки. Плюс обёртка вокруг запроса и часто отдельная очистка. React 19 убирает эту рутину: каждый из четырёх хуков берёт на себя один такой кусок. Ниже разберём их по очереди. Слева — работающий пример, справа — что в этот момент происходит с состоянием.

Минимум: react@19.0.0 stable
хук 01 — чтение ресурсов в рендере

use — промис прямо в рендере

Читает промис или контекст прямо во время рендера. При этом нарушает привычное правило хуков: его можно вызывать условно, внутри цикла и даже внутри if. Если передать промис, компонент приостанавливается через Suspense, пока данные не загрузятся.

Демо вживую
Что внутри

Пример показывает, как ведёт себя хук: сначала заглушка Suspense, потом загруженные данные. Рядом — настоящий код React 19.

UserProfile.jsx

				

Условное чтение контекста

Обычные хуки нельзя вызывать после раннего return. use — можно. Поэтому им удобно читать контекст точечно:

conditional-context.jsx

				
Ловушка

Промис нельзя создавать внутри клиентского компонента (use(fetch(...)) в теле) — на каждый рендер будет новый промис и бесконечный цикл. Промис должен приходить из фреймворка или библиотеки с кешированием (в SPA — из внешнего кеша/лоадера). Для контекста этого ограничения нет.

хук 02 — мгновенный UI

useOptimistic — показать до ответа сервера

Показываешь изменение в интерфейсе сразу, ещё до ответа сервера. Если сервер отказал, React сам возвращает настоящее состояние. Чаты, лайки, комментарии — везде, где ждать ответ неприятно.

Демо вживую
Настоящее vs оптимистичное

Включи «Сервер падает» и добавь комментарий — увидишь, как оптимистичный элемент появляется мгновенно, а потом откатывается.

Comments.jsx

				
Как это устроено. Настоящее состояние — это comments, которые пришли с сервера. useOptimistic показывает поверх них временную версию. Когда сервер отвечает успехом, настоящее состояние обновляется и временная версия заменяется им. Если сервер отказал и настоящее состояние не изменилось — временная версия просто исчезает.
Две ловушки

1. addOptimistic обязательно внутри transition (в action, в startTransition или в <form action>). Вызовешь в голом setTimeout или обычном обработчике — элемент застрянет навсегда.

2. Откат срабатывает только когда меняется настоящее состояние. Если действие тихо провалилось и не тронуло его, оптимистичное значение так и останется на экране. Поэтому возвращай новое состояние даже при ошибке.

хук 03 — состояние действия

useActionState — форма без трёх useState

Управляет жизненным циклом действия: результат, ошибка, pending — в одном хуке. Заменяет привычную связку isLoading + error + data.

Демо вживую
[state, isPending]

Пароль 1234 — успех, любой другой — ошибка. Смотри, как isPending переключается сам.

LoginForm.jsx

				
Важно про миграцию

useActionState пришёл на замену экспериментальному useFormState из canary-сборок react-dom. API почти тот же, но добавлен третий возврат — isPending. И импорт теперь из 'react', а не 'react-dom'.

хук 04 — статус формы без пропсов

useFormStatus — кнопка знает сама

Даёт вложенному компоненту статус ближайшей родительской <form> без прокидывания пропсов и контекста. Классика — переиспользуемая кнопка Submit, которая сама гаснет на время отправки.

Демо вживую
pending по расположению

Кнопка внутри формы видит pending. Кнопка «снаружи» — всегда false. Это и есть главная ловушка, показанная вживую.

SubmitButton.jsx

				
Ловушка расположения

useFormStatus привязан к ближайшей родительской <form>. Поставишь компонент вне формы — pending навсегда останется false. Кнопка должна рендериться внутри формы, а не рядом с ней.

итог

Шпаргалка

Что откуда импортируется и зачем нужно. Импорт путает чаще всего.

Хук Импорт Зачем
use react Читать промис/контекст в рендере. Можно условно.
useOptimistic react Мгновенный UI + автооткат при провале.
useActionState react state + isPending для действия/формы. Замена useFormState.
useFormStatus react-dom Статус родительской формы без пропсов.
Границы применимости. Эти хуки — про состояние действия на уровне компонента. Они не заменяют React Query / SWR / Redux: те про синхронизацию глобального серверного кеша и инвалидацию по многим компонентам. Мутация с общим кешем — всё ещё их территория.
закрепление

Практика

Четыре задания — по одному на каждый хук. Всё на чистом клиентском React. «Запрос на сервер» везде подделываем через setTimeout, настоящий бэкенд не нужен.

Лайк с откатом

Кнопка-сердечко со счётчиком лайков. По клику число растёт сразу, но запрос иногда падает — и тогда лайк откатывается назад.

Шаги
  1. Заведи настоящее состояние — число лайков через useState, например 10.
  2. Оберни его в useOptimistic. Функция обновления берёт текущее число и прибавляет 1.
  3. По клику вызови addOptimistic() внутри startTransition, а следом «отправь запрос».
  4. Запрос — фейковый: setTimeout, который в 30% случаев завершается ошибкой. При успехе увеличь настоящее состояние на 1. При ошибке не делай ничего — React вернёт число сам.

Подсказкаawait new Promise((res, rej) => setTimeout(() => Math.random() < 0.3 ? rej() : res(), 800))

ГотовоКогда запрос падает, счётчик подпрыгивает на +1 и тут же возвращается на место.

Форма подписки

Поле email и кнопка. Пока идёт отправка — кнопка заблокирована и пишет «Отправляем…». Потом либо «Готово», либо ошибка, если email пустой или без символа @.

Шаги
  1. Напиши async-функцию действия subscribeAction(prevState, formData). Достань email через formData.get('email').
  2. Проверь его: если пусто или нет @ — верни { error: 'Введите email' }. Иначе подожди секунду и верни { ok: true }.
  3. В компоненте: const [state, action, isPending] = useActionState(subscribeAction, null).
  4. Повесь action на <form action={action}>, блокируй кнопку по isPending, покажи state.error или state.ok.

ПодсказкаИмпорт useActionState из 'react', а не из 'react-dom'. Проверку делай прямо внутри функции действия — отдельный useState для ошибки больше не нужен.

ГотовоПустой email даёт ошибку сразу. Правильный — «Отправляем…», затем «Готово».

Переиспользуемая кнопка

Вынести кнопку отправки в отдельный компонент, который сам знает, идёт ли отправка формы — без единого пропса. Вставить его в две разные формы.

Шаги
  1. Сделай компонент SubmitButton. Внутри — const { pending } = useFormStatus(). Кнопка блокируется и меняет текст по pending.
  2. Собери две разные формы: у каждой свой action и свои поля.
  3. В каждую форму вставь <SubmitButton /> — ничего ей не передавая.
  4. Отправь по очереди обе формы и посмотри на кнопки.

ПодсказкаuseFormStatus — из 'react-dom'. Кнопка обязана быть внутри <form>: если поставить рядом, pending всегда останется false.

ГотовоОбе кнопки гаснут во время своей отправки, хотя ты не прокидывал им состояние.

Профиль через use

Компонент читает данные пользователя через use(userPromise) и показывает имя и роль. Пока грузится — Suspense показывает заглушку.

Шаги
  1. Создай промис один раз снаружи компонента — в модуле или в родителе: const userPromise = fetchUser().
  2. В компоненте Profile: const user = use(userPromise), выведи user.name и user.role.
  3. Оберни <Profile /> в <Suspense fallback={<p>Загрузка…</p>}>.
  4. fetchUser — снова фейк: промис с setTimeout, который через секунду отдаёт объект пользователя.

ПодсказкаЕсли создать промис прямо в теле компонента, на каждый рендер будет новый промис и бесконечная загрузка. Промис живёт снаружи.

ГотовоСначала видно «Загрузка…», потом — профиль. Спиннер не крутится вечно.