React
Actions и useActionState
Форма входа в классическом React - это маленький проект: useState под каждое поле, useState под loading, useState под текст ошибки, обработчик onSubmit с preventDefault, ручная блокировка кнопки. И всё это ломается, если пользователь нажал отправить до того, как загрузился JS - форма просто молчит. React 19 ввёл Actions: форме передаётся функция в проп action, а хук useActionState возвращает состояние, обёрнутый action и флаг ожидания. Форма при этом работает даже до гидрации, как обычная HTML-форма.
- React 19 (декабрь 2024): проп action у form, useActionState и поддержка асинхронных функций как action стабилизированы
- Next.js App Router: формы через action + Server Function - дефолтный способ обрабатывать сабмиты с прогрессивным улучшением
- React Docs: документация описывает Actions, useActionState и переименование из прежнего useFormState
- Vercel templates: стартеры с логином и оплатой строят формы на useActionState, возвращая ошибки как состояние
- Remix: идея серверных action'ов и работающих без JS форм давно проверена в проде и теперь вошла в ядро React
Предварительные знания
- Server Functions и директива 'use server', возврат значений клиенту
- Хук useState: состояние, которое сохраняется между рендерами
- Базовое понимание HTML-форм: form, поля ввода, submit
Как React вернул формам способность работать без JavaScript
До эры SPA HTML-форма работала сама по себе: браузер собирал поля и отправлял POST на сервер без единой строки JS. SPA-подход это сломал - всё пошло через onSubmit и preventDefault, и без загруженного JS форма становилась мёртвой. React 19 вернул старую надёжность в новой обёртке. Проп action принимает функцию, а не URL, но если эта функция серверная, форма умеет отправиться нативным POST ещё до гидрации. Хук, который раньше назывался useFormState, переименовали в useActionState и расширили: теперь он отдаёт ещё и флаг isPending. Так формы снова стали устойчивыми к медленному JS, не теряя интерактивности.
Проп action у формы
В React 19 у элемента form проп action принимает не URL, а функцию. При сабмите React сам собирает поля формы в объект FormData и передаёт его в эту функцию. Не нужен ни обработчик onSubmit, ни preventDefault, ни ручное чтение значений из полей. Функция-action может быть клиентской или серверной (Server Function) - в обоих случаях форма знает, что делать при отправке.
Значения полей читаются по атрибуту name через formData.get. Это та же модель, что у нативных HTML-форм: имя поля - ключ. Благодаря этому форма работоспособна даже без React: если JS ещё не загрузился, браузер отправит обычный POST с теми же полями, а серверный action их примет.
Проп action принимает и кнопку: у элемента button есть formAction, позволяющий одной форме иметь несколько действий (например, 'Сохранить' и 'Сохранить и опубликовать'). Каждая кнопка задаёт свою функцию, а поля у них общие.
Что получает функция, переданная в проп action формы, при сабмите?
useActionState: состояние, action и isPending
Голый проп action ничего не сообщает о результате: получилось ли, какая ошибка, идёт ли отправка. Это решает хук useActionState. Он принимает action и начальное состояние, а возвращает кортеж из трёх элементов: текущее состояние, обёрнутый formAction для передачи в форму и флаг isPending. Сигнатура: const [state, formAction, isPending] = useActionState(action, initialState).
Action, обёрнутый useActionState, получает на вход дополнительный первый аргумент - предыдущее состояние, а вторым уже FormData. Его сигнатура: (prevState, formData) => newState. То, что вернёт action, станет новым state на следующем рендере. Так результат каждого сабмита естественно попадает в UI без отдельного useState под ошибку или успех.
| Элемент кортежа | Что это | Куда идёт |
|---|---|---|
| state | Последнее значение, возвращённое action | Рендерится в форме (ошибки, успех) |
| formAction | Обёрнутый action | Передаётся в проп action формы |
| isPending | true, пока action выполняется | Дизейбл кнопки, текст ожидания |
isPending снимает целый класс ручного кода. Раньше под загрузку заводили отдельный useState, выставляли true в начале и false в конце, не забывая про ветку с ошибкой. Теперь флаг ведёт сам React: пока action в полёте, isPending равен true.
Что возвращает вызов useActionState(action, initialState)?
Возврат ошибок и прогрессивное улучшение
Ключевое правило: action должен возвращать ошибки, а не бросать их. Возвращённое значение становится новым state, который рендерится в форме - например, текст 'Email уже занят' под полем. Если же бросить исключение, оно улетит к ближайшему error boundary и снесёт весь экран вместо того, чтобы показать сообщение в форме. Брошенные исключения уместны для неожиданных сбоев, а ожидаемые ошибки валидации - это данные.
- Вернуть ошибку (правильно) — return { error: 'Email уже занят' }. Значение становится state, форма рендерит сообщение под полем, пользователь остаётся на странице и правит ввод.
- Бросить ошибку (обычно неверно) — throw new Error('Email занят'). Исключение ловит error boundary, весь экран заменяется на fallback, контекст формы и введённые данные теряются.
Второе важное свойство - прогрессивное улучшение. Форма с серверным action работает как обычная HTML-форма ещё до того, как загрузится и отработает клиентский JS (гидрация). Если пользователь успел нажать отправить раньше гидрации, React не теряет это нажатие: после загрузки JS действие воспроизводится. Поэтому медленная сеть или большой бандл не делают форму неотзывчивой - базовая отправка работает с самого начала.
Прогрессивное улучшение работает в полной мере, когда action - серверная функция и форма построена на нативных полях с атрибутами name. Если завязать логику на onClick клиентской кнопки или собирать значения из состояния вместо полей, форма перестанет отправляться без JS, и устойчивость к медленной гидрации потеряется.
Почему ожидаемую ошибку валидации лучше возвращать из action, а не бросать через throw?
Связь с другими темами
Actions - клиентская обвязка серверных действий. Что рядом:
- useOptimistic и useFormStatus — Достраивают форму: мгновенный оптимистичный UI и состояние ожидания для кнопок и спиннеров
- Server Functions — Action формы обычно и есть Server Function; валидация и возврат ошибок переходят сюда
- Next.js App Router — Фреймворк связывает action формы с роутингом, кэшем и прогрессивным улучшением
Итог
- Проп action у формы принимает функцию (часто Server Function); при сабмите React вызывает её с объектом FormData
- useActionState(action, initial) возвращает кортеж [state, formAction, isPending]: текущее состояние, обёрнутый action для формы и флаг ожидания
- Action должен ВОЗВРАЩАТЬ ошибки как часть state, а не бросать исключение: возвращённое значение становится новым state и рендерится в форме
- isPending снимает ручное ведение loading-флага: пока action выполняется, он true, и кнопку можно дизейблить без отдельного useState
- Прогрессивное улучшение: форма с серверным action отправляется нативным POST ещё до гидрации, а нажатия до загрузки JS воспроизводятся после неё
Связанные уроки
- rc-33-useoptimistic-formstatus — useOptimistic и useFormStatus достраивают форму поверх Actions: оптимистичный UI и состояние pending у кнопок
- rc-31-server-functions — Action формы - это чаще всего Server Function, поэтому возврат ошибок и валидация продолжают предыдущий урок
- rc-06-usestate — useActionState обобщает идею useState на асинхронный результат серверного действия