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 формы
isPendingtrue, пока 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 на асинхронный результат серверного действия
Actions и useActionState

0

1

Войти