useActionState в React 19

16 июня 2026
Автор

Антон Ларичев

Что такое useActionState

useActionState — хук, появившийся в React 19, который упрощает управление состоянием формы при выполнении асинхронных действий (actions). Он заменяет паттерн с ручным useState + try/catch + флагом загрузки и предоставляет единый интерфейс для:

  • хранения результата последнего вызова action;
  • отслеживания pending-состояния;
  • обновления состояния компонента после завершения action.

До React 19 для отправки формы и отображения ошибок приходилось писать:

const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);

async function handleSubmit(e: React.FormEvent) {
  e.preventDefault();
  setLoading(true);
  try {
    await submitData(formData);
  } catch (err) {
    setError('Ошибка отправки');
  } finally {
    setLoading(false);
  }
}

С useActionState весь этот шаблонный код сводится к нескольким строкам.

Синтаксис

const [state, formAction, isPending] = useActionState(action, initialState, permalink?);

Параметры

  • action — асинхронная функция (prevState, formData) => newState. Вызывается при отправке формы или явно через возвращённый formAction.
  • initialState — начальное значение state. Может быть любым сериализуемым значением: null, объект с полями ошибок, строка и т.д.
  • permalink (необязательный) — URL страницы. Используется при SSR и прогрессивном улучшении: если JS ещё не загрузился, браузер перенаправит на этот URL после отправки формы.

Возвращаемые значения

  • state — текущее состояние. При первом рендере равно initialState, после вызова action — возвращаемому значению action.
  • formAction — функция, которую нужно передать в action элемента <form> или в onClick кнопки.
  • isPendingtrue, пока action выполняется. Позволяет блокировать форму или показывать индикатор загрузки.

Базовый пример: форма входа

import { useActionState } from 'react';

type LoginState = {
  error: string | null;
  success: boolean;
};

async function loginAction(
  prevState: LoginState,
  formData: FormData
): Promise<LoginState> {
  const email = formData.get('email') as string;
  const password = formData.get('password') as string;

  const response = await fetch('/api/login', {
    method: 'POST',
    body: JSON.stringify({ email, password }),
    headers: { 'Content-Type': 'application/json' },
  });

  if (!response.ok) {
    return { error: 'Неверный email или пароль', success: false };
  }

  return { error: null, success: true };
}

export function LoginForm() {
  const [state, formAction, isPending] = useActionState(loginAction, {
    error: null,
    success: false,
  });

  if (state.success) {
    return <p>Вы успешно вошли!</p>;
  }

  return (
    <form action={formAction}>
      <input name="email" type="email" placeholder="Email" required />
      <input name="password" type="password" placeholder="Пароль" required />

      {state.error && <p style={{ color: 'red' }}>{state.error}</p>}

      <button type="submit" disabled={isPending}>
        {isPending ? 'Вхожу...' : 'Войти'}
      </button>
    </form>
  );
}

Обратите внимание: formAction передаётся напрямую в атрибут action тега <form>. React перехватывает отправку формы и вызывает action с объектом FormData.

Обработка ошибок валидации

Часто нужно валидировать несколько полей и возвращать сообщения для каждого:

type RegisterState = {
  errors: {
    name?: string;
    email?: string;
    password?: string;
  };
  success: boolean;
};

async function registerAction(
  prevState: RegisterState,
  formData: FormData
): Promise<RegisterState> {
  const name = (formData.get('name') as string).trim();
  const email = (formData.get('email') as string).trim();
  const password = formData.get('password') as string;

  const errors: RegisterState['errors'] = {};

  if (!name) errors.name = 'Имя обязательно';
  if (!email.includes('@')) errors.email = 'Укажите корректный email';
  if (password.length < 8) errors.password = 'Минимум 8 символов';

  if (Object.keys(errors).length > 0) {
    return { errors, success: false };
  }

  await createUser({ name, email, password });
  return { errors: {}, success: true };
}

export function RegisterForm() {
  const [state, formAction, isPending] = useActionState(registerAction, {
    errors: {},
    success: false,
  });

  return (
    <form action={formAction}>
      <div>
        <input name="name" placeholder="Имя" />
        {state.errors.name && <span>{state.errors.name}</span>}
      </div>
      <div>
        <input name="email" type="email" placeholder="Email" />
        {state.errors.email && <span>{state.errors.email}</span>}
      </div>
      <div>
        <input name="password" type="password" placeholder="Пароль" />
        {state.errors.password && <span>{state.errors.password}</span>}
      </div>
      <button type="submit" disabled={isPending}>
        {isPending ? 'Регистрация...' : 'Зарегистрироваться'}
      </button>
    </form>
  );
}

Использование с Server Actions в Next.js

Основная сила useActionState проявляется в связке с Server Actions. Action выполняется на сервере, а результат автоматически возвращается клиенту.

// app/actions/newsletter.ts
'use server';

import { db } from '@/lib/db';

type NewsletterState = {
  message: string | null;
  type: 'success' | 'error' | null;
};

export async function subscribeAction(
  prevState: NewsletterState,
  formData: FormData
): Promise<NewsletterState> {
  const email = formData.get('email') as string;

  if (!email || !email.includes('@')) {
    return { message: 'Введите корректный email', type: 'error' };
  }

  const exists = await db.subscriber.findUnique({ where: { email } });
  if (exists) {
    return { message: 'Вы уже подписаны', type: 'error' };
  }

  await db.subscriber.create({ data: { email } });
  return { message: 'Вы успешно подписались!', type: 'success' };
}
// app/components/NewsletterForm.tsx
'use client';

import { useActionState } from 'react';
import { subscribeAction } from '@/app/actions/newsletter';

export function NewsletterForm() {
  const [state, formAction, isPending] = useActionState(subscribeAction, {
    message: null,
    type: null,
  });

  return (
    <form action={formAction}>
      <input name="email" type="email" placeholder="Ваш email" />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Отправка...' : 'Подписаться'}
      </button>
      {state.message && (
        <p style={{ color: state.type === 'error' ? 'red' : 'green' }}>
          {state.message}
        </p>
      )}
    </form>
  );
}

Server Action имеет доступ к базе данных, ORM, переменным окружения — и всё это без единого API-маршрута.

Доступ к предыдущему состоянию

Первым аргументом action всегда получает предыдущее state. Это позволяет накапливать данные или реализовывать логику "retry":

type RetryState = {
  attempts: number;
  lastError: string | null;
  success: boolean;
};

async function submitWithRetry(
  prevState: RetryState,
  formData: FormData
): Promise<RetryState> {
  if (prevState.attempts >= 3) {
    return {
      ...prevState,
      lastError: 'Превышен лимит попыток. Попробуйте позже.',
    };
  }

  try {
    await riskyOperation(formData);
    return { attempts: prevState.attempts + 1, lastError: null, success: true };
  } catch {
    return {
      attempts: prevState.attempts + 1,
      lastError: `Попытка ${prevState.attempts + 1} не удалась`,
      success: false,
    };
  }
}

export function RetryForm() {
  const [state, formAction, isPending] = useActionState(submitWithRetry, {
    attempts: 0,
    lastError: null,
    success: false,
  });

  return (
    <form action={formAction}>
      <button type="submit" disabled={isPending || state.attempts >= 3}>
        Отправить
      </button>
      {state.lastError && <p>{state.lastError}</p>}
      {state.attempts > 0 && !state.success && (
        <p>Попыток: {state.attempts} из 3</p>
      )}
    </form>
  );
}

Вызов action вне формы

formAction можно вызывать программно, не только через <form>. Например, при нажатии на кнопку без формы:

export function DeleteButton({ itemId }: { itemId: string }) {
  const [state, deleteAction, isPending] = useActionState(
    async (prevState: { deleted: boolean }, formData: FormData) => {
      await deleteItem(formData.get('id') as string);
      return { deleted: true };
    },
    { deleted: false }
  );

  if (state.deleted) return <p>Элемент удалён</p>;

  return (
    <form action={deleteAction}>
      <input type="hidden" name="id" value={itemId} />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Удаление...' : 'Удалить'}
      </button>
    </form>
  );
}

Сравнение с альтернативами

useActionState vs useState + useTransition

// До React 19: ручное управление
function ManualForm() {
  const [error, setError] = useState<string | null>(null);
  const [isPending, startTransition] = useTransition();

  function handleSubmit(formData: FormData) {
    startTransition(async () => {
      const result = await submitAction(formData);
      if (result.error) setError(result.error);
    });
  }

  return (
    <form action={handleSubmit}>
      {error && <p>{error}</p>}
      <button disabled={isPending}>Отправить</button>
    </form>
  );
}

// React 19: useActionState
function ModernForm() {
  const [state, formAction, isPending] = useActionState(submitAction, {
    error: null,
  });

  return (
    <form action={formAction}>
      {state.error && <p>{state.error}</p>}
      <button disabled={isPending}>Отправить</button>
    </form>
  );
}

useActionState — это не просто синтаксический сахар. Он правильно интегрирован с React Concurrent Mode, поддерживает Server Actions и прогрессивное улучшение без JS.

useActionState vs react-hook-form

react-hook-form подходит для сложных форм с динамическими полями, зависимой валидацией и rich UX (валидация по blur, debounce). useActionState оптимален для простых форм с серверной логикой, особенно в Next.js App Router.

Прогрессивное улучшение

Одно из главных преимуществ useActionState — форма работает даже без JavaScript. Браузер отправляет форму нативным способом, а параметр permalink указывает, куда перенаправить пользователя после ответа сервера:

const [state, formAction] = useActionState(
  subscribeAction,
  { message: null },
  '/newsletter/confirm' // редирект при работе без JS
);

Это особенно важно для SEO и доступности: форма функционирует даже при медленной загрузке или отключённом JS.

Типичные ошибки

Мутация prevState

// Неправильно — мутируем объект
async function badAction(prevState: State, formData: FormData) {
  prevState.count++; // ошибка!
  return prevState;
}

// Правильно — возвращаем новый объект
async function goodAction(prevState: State, formData: FormData) {
  return { ...prevState, count: prevState.count + 1 };
}

Несериализуемое состояние с Server Actions

При использовании Server Actions состояние сериализуется и передаётся через сеть. Не используйте функции, классовые экземпляры или Symbol:

// Неправильно
return { callback: () => doSomething() };

// Правильно
return { status: 'done', id: '123' };

Забытый await в action

// Неправильно — action вернёт Promise, а не результат
async function action(prevState: State, formData: FormData) {
  return fetch('/api/data'); // забыли await
}

// Правильно
async function action(prevState: State, formData: FormData) {
  const res = await fetch('/api/data');
  return res.json();
}

Итог

useActionState — это правильный способ работы с формами в React 19+. Он убирает шаблонный код управления состоянием, корректно интегрируется с Server Actions в Next.js и поддерживает прогрессивное улучшение из коробки.

Ключевые моменты:

  • Action получает (prevState, formData) и возвращает новый state.
  • isPending — встроенный флаг загрузки, не нужен отдельный useState.
  • Для Server Actions состояние должно быть сериализуемым.
  • permalink обеспечивает работу формы без JavaScript.

Чтобы освоить React 19, Server Actions и современные паттерны работы с формами на практике, изучите курс React с нуля до PRO на PurpleSchool.

Стрелочка влевоuseCallback в React — мемоизация функций и оптимизация ре-рендеровОптимизация рендеринга в React: от теории к глубокой практикеСтрелочка вправо

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

Uncontrolled Components: когда DOM управляет даннымиБезопасность в React: защита от XSS, CSRF и утечек данныхRender Props: гибкое управление рендерингом в ReactРефакторинг React-кода: техники и лучшие практикиПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаИменование компонентов в React: соглашения и лучшие практикиЛенивая загрузка: как ускорить React-приложение в разыHOC в React: мастерство композиции компонентовuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIДокументирование компонентов в React: Storybook, JSDoc и READMEКомпозиция компонентов в React: строим гибкие интерфейсыКомментирование кода в React: когда и как писать комментарииCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с даннымиДоступность (a11y) в React: ARIA, семантика и клавиатурная навигация
Zustand — управление состоянием в ReactZod - валидация с TypeScriptYup - валидация схемТемизация в ReactТестирование хуковTailwind CSS с ReactXState - конечные автоматыSWR - библиотека для запросовStyled Components — стилизация через JSStorybook - документация компонентовSnapshots тестированиеRTK Query - работа с APIRedux Toolkit - современный ReduxRecoil — библиотека управления состоянием от FacebookВиртуализация списков с react-window: как отображать тысячи элементов без лаговReact Toastify - уведомления в ReactReact Testing LibraryСоздание таблиц в React гайд по react-tableReact Spring - анимацииРабота с формами и селектами в ReactReact Query (TanStack Query) - работа с серверомПлагины в React что это и как их использоватьReact PDF - работа с PDF файламиОбзор популярных библиотек для ReactReact Icons - библиотека иконок для ReactReact Hook Form — валидация форм в ReactReact Dropzone — загрузка файловПодключение Bootstrap к React-приложениюReact Beautiful DnD - перетаскивание элементовАнимация при монтировании компонентов в ReactМокирование APIMobX — реактивное управление состоянием в ReactМикрофронтенды с React (micro-frontends)Загрузка и индикаторыАнимация списков в ReactJotai - атомарное состояниеБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactE2E тестирование с CypressCSSTransition - переходыCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовОбработка ошибок API
useState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseFormStatus - отслеживание статуса отправки формыuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияuseEffect в React что это и как использоватьuseContext — работа с контекстом в ReactuseCallback в React — мемоизация функций и оптимизация ре-рендеровuseActionState в React 19Оптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextКак предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
Transition API — плавные обновления интерфейса в ReactReact Suspense — приостановка рендераStrictMode в React — как находить ошибки на этапе разработкиСерверные компоненты React (RSC) — подробный разбор и практикаКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMFragment в React: группировка элементов без лишних узлов DOMCSS Modules в ReactConcurrent Mode — конкурентный режим в React
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий