useFormStatus - отслеживание статуса отправки формы

06 ноября 2026
Автор

Олег Марков

Введение

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

Хук useFormStatus решает эту проблему элегантно: он позволяет любому дочернему компоненту формы получить информацию о текущем статусе её отправки — без лишних пропсов и передачи состояния вручную.

Если вы хотите глубже изучить React и современные подходы к разработке форм, рекомендую курс по React на PurpleSchool.

Курс по теме

Курс React и Redux Toolkit с нуля

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Что такое useFormStatus

useFormStatus — это хук из пакета react-dom, появившийся в React 19. Он предоставляет информацию о состоянии ближайшей родительской формы (<form>). Хук возвращает объект с полями, описывающими текущий статус отправки.

Ключевая особенность: хук работает только внутри компонента, который является дочерним по отношению к элементу <form>. Он не работает в самом компоненте формы — только в его потомках.

Синтаксис

import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending, data, method, action } = useFormStatus();
  // ...
}

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

Хук возвращает объект со следующими полями:

ПолеТипОписание
pendingbooleantrue, если форма сейчас отправляется
dataFormData | nullДанные, которые отправляет форма
methodstringHTTP-метод формы ("get" или "post")
actionstring | function | nullАтрибут action формы или Server Action

Самое используемое поле — pending. Именно оно позволяет реагировать на момент ожидания ответа сервера.

Зачем нужен useFormStatus

Рассмотрим типичную проблему: нужно заблокировать кнопку отправки, пока форма обрабатывается, и показать текст «Отправка...».

Без useFormStatus (классический подход)

import React, { useState } from 'react';

function ContactForm() {
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsSubmitting(true);
    try {
      await sendContactForm(new FormData(e.target as HTMLFormElement));
    } finally {
      setIsSubmitting(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="email" type="email" placeholder="Email" />
      <textarea name="message" placeholder="Сообщение" />
      {/* Приходится передавать isSubmitting в кнопку */}
      <SubmitButton isSubmitting={isSubmitting} />
    </form>
  );
}

function SubmitButton({ isSubmitting }: { isSubmitting: boolean }) {
  return (
    <button type="submit" disabled={isSubmitting}>
      {isSubmitting ? 'Отправка...' : 'Отправить'}
    </button>
  );
}

Здесь isSubmitting нужно явно передавать через пропсы. При глубокой вложенности компонентов это превращается в «пробрасывание пропсов» (prop drilling).

С useFormStatus (современный подход)

import { useFormStatus } from 'react-dom';

// Компонент кнопки сам знает о статусе формы
function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Отправка...' : 'Отправить'}
    </button>
  );
}

function ContactForm() {
  async function handleSubmit(formData: FormData) {
    await sendContactForm(formData);
  }

  return (
    <form action={handleSubmit}>
      <input name="email" type="email" placeholder="Email" />
      <textarea name="message" placeholder="Сообщение" />
      {/* Кнопке не нужны дополнительные пропсы */}
      <SubmitButton />
    </form>
  );
}

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

Основные примеры использования

Пример 1: Кнопка отправки с индикатором загрузки

Самый распространённый кейс — блокировка кнопки и смена её текста.

import { useFormStatus } from 'react-dom';

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button
      type="submit"
      disabled={pending}
      className={`btn ${pending ? 'btn-loading' : 'btn-primary'}`}
    >
      {pending ? (
        <>
          <span className="spinner" />
          Сохраняем...
        </>
      ) : (
        'Сохранить'
      )}
    </button>
  );
}

// Использование в форме
function ProfileForm() {
  async function updateProfile(formData: FormData) {
    'use server';
    await saveProfileToDatabase({
      name: formData.get('name') as string,
      bio: formData.get('bio') as string,
    });
  }

  return (
    <form action={updateProfile}>
      <input name="name" placeholder="Имя" defaultValue="Иван Петров" />
      <textarea name="bio" placeholder="О себе" />
      <SubmitButton />
    </form>
  );
}

Пример 2: Блокировка всей формы во время отправки

Иногда нужно заблокировать не только кнопку, но и все поля формы.

import { useFormStatus } from 'react-dom';

function FormField({
  name,
  label,
  type = 'text',
}: {
  name: string;
  label: string;
  type?: string;
}) {
  const { pending } = useFormStatus();

  return (
    <div className="form-group">
      <label htmlFor={name}>{label}</label>
      <input
        id={name}
        name={name}
        type={type}
        disabled={pending}
        className={pending ? 'input-disabled' : ''}
      />
    </div>
  );
}

function RegisterForm() {
  async function registerUser(formData: FormData) {
    'use server';
    await createUserAccount({
      email: formData.get('email') as string,
      password: formData.get('password') as string,
      name: formData.get('name') as string,
    });
  }

  return (
    <form action={registerUser}>
      <FormField name="name" label="Имя" />
      <FormField name="email" label="Email" type="email" />
      <FormField name="password" label="Пароль" type="password" />
      <SubmitButton />
    </form>
  );
}

Пример 3: Отображение прогресса с данными формы

Поле data позволяет показать, какие данные отправляются прямо сейчас.

import { useFormStatus } from 'react-dom';

function FormStatus() {
  const { pending, data } = useFormStatus();

  if (!pending || !data) return null;

  const email = data.get('email');

  return (
    <div className="form-status">
      <span className="spinner" />
      <p>
        Отправляем на <strong>{email as string}</strong>...
      </p>
    </div>
  );
}

function NewsletterForm() {
  async function subscribe(formData: FormData) {
    'use server';
    await addToNewsletter(formData.get('email') as string);
  }

  return (
    <form action={subscribe}>
      <input name="email" type="email" placeholder="Ваш email" />
      <FormStatus />
      <button type="submit">Подписаться</button>
    </form>
  );
}

Интеграция с Next.js Server Actions

useFormStatus особенно полезен в Next.js App Router, где формы часто работают с Server Actions.

Базовый пример с Server Action

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

import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  // Имитируем задержку сервера
  await new Promise((resolve) => setTimeout(resolve, 1500));

  await db.post.create({ data: { title, content } });
  revalidatePath('/posts');
}
// app/posts/new/page.tsx
import { createPost } from '../actions';
import { SubmitButton } from '@/components/SubmitButton';

export default function NewPostPage() {
  return (
    <div className="container">
      <h1>Новая запись</h1>
      <form action={createPost}>
        <div>
          <label htmlFor="title">Заголовок</label>
          <input id="title" name="title" required />
        </div>
        <div>
          <label htmlFor="content">Содержание</label>
          <textarea id="content" name="content" rows={10} required />
        </div>
        <SubmitButton />
      </form>
    </div>
  );
}
// components/SubmitButton.tsx
'use client';

import { useFormStatus } from 'react-dom';

export function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Публикуем...' : 'Опубликовать'}
    </button>
  );
}

Обратите внимание: компонент с useFormStatus должен быть клиентским ('use client'), потому что useFormStatus — это клиентский хук. Но форма и Server Action могут оставаться серверными.

Паттерн: переиспользуемый компонент кнопки

В реальных проектах удобно создать универсальную кнопку отправки.

// components/SubmitButton.tsx
'use client';

import { useFormStatus } from 'react-dom';
import type { ReactNode } from 'react';

interface SubmitButtonProps {
  children: ReactNode;
  loadingText?: string;
  className?: string;
}

export function SubmitButton({
  children,
  loadingText = 'Загрузка...',
  className = '',
}: SubmitButtonProps) {
  const { pending } = useFormStatus();

  return (
    <button
      type="submit"
      disabled={pending}
      aria-disabled={pending}
      className={`submit-btn ${pending ? 'submit-btn--loading' : ''} ${className}`}
    >
      {pending ? loadingText : children}
    </button>
  );
}

Использование:

// В любой форме
<form action={someServerAction}>
  {/* ... поля ... */}
  <SubmitButton loadingText="Сохраняем профиль...">
    Сохранить профиль
  </SubmitButton>
</form>

// В другой форме
<form action={anotherAction}>
  {/* ... поля ... */}
  <SubmitButton loadingText="Отправляем...">
    Отправить сообщение
  </SubmitButton>
</form>

Как работает useFormStatus под капотом

useFormStatus использует React Context внутри реализации форм. Когда форма начинает отправку (через нативный механизм <form> или Server Action), React автоматически обновляет контекст формы, который потребляют все дочерние компоненты, использующие useFormStatus.

Это значит:

  • Нет лишних ре-рендеров родительского компонента — статус обновляется только в тех компонентах, которые его используют.
  • Автоматическая синхронизация — вам не нужно вручную управлять состоянием загрузки.
  • Работает с нативными формами — хук реагирует на отправку <form> через action атрибут.

Важное ограничение: место вызова

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

// ❌ НЕПРАВИЛЬНО — useFormStatus вызывается в компоненте формы
function Form() {
  const { pending } = useFormStatus(); // Всегда вернёт { pending: false }!

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

// ✅ ПРАВИЛЬНО — useFormStatus вызывается в дочернем компоненте
function SubmitButton() {
  const { pending } = useFormStatus(); // Корректно отслеживает форму

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Отправка...' : 'Отправить'}
    </button>
  );
}

function Form() {
  return (
    <form action={handleSubmit}>
      <SubmitButton /> {/* Корректный дочерний компонент */}
    </form>
  );
}

Поле action: работа с различными типами

Поле action из useFormStatus может принимать разные значения в зависимости от атрибута action формы.

import { useFormStatus } from 'react-dom';

function FormDebugInfo() {
  const { pending, method, action } = useFormStatus();

  if (!pending) return null;

  return (
    <div className="debug-info">
      <p>Метод: {method}</p>
      <p>
        Действие:{' '}
        {typeof action === 'function'
          ? 'Server Action'
          : action || 'не указано'}
      </p>
    </div>
  );
}

// Форма с URL в action
<form action="/api/submit" method="post">
  <FormDebugInfo /> {/* action = "/api/submit", method = "post" */}
  <SubmitButton />
</form>

// Форма с Server Action
<form action={serverAction}>
  <FormDebugInfo /> {/* action = [Function], method = "post" */}
  <SubmitButton />
</form>

Обработка ошибок и повторная отправка

useFormStatus автоматически возвращает pending: false после завершения отправки — независимо от успеха или ошибки. Для отображения ошибок используйте отдельное состояние или useActionState.

'use client';

import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';
import { submitForm } from './actions';

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Отправка...' : 'Отправить'}
    </button>
  );
}

type FormState = {
  error?: string;
  success?: boolean;
};

export function ContactForm() {
  const [state, action] = useActionState<FormState, FormData>(
    submitForm,
    {}
  );

  return (
    <form action={action}>
      <input name="email" type="email" placeholder="Email" required />
      <textarea name="message" placeholder="Сообщение" required />

      {state.error && (
        <p className="error-message">{state.error}</p>
      )}

      {state.success && (
        <p className="success-message">Сообщение успешно отправлено!</p>
      )}

      <SubmitButton />
    </form>
  );
}

Сравнение useFormStatus с другими подходами

useFormStatus vs ручное управление состоянием

КритерийРучной useStateuseFormStatus
КодБольше бойлерплейтаКомпактный
Передача состоянияЧерез пропсыАвтоматически
ПереиспользуемостьОграниченаВысокая
Поддержка Server ActionsНужна доработкаВстроена
React версияЛюбаяReact 19+

useFormStatus vs useActionState

Эти два хука дополняют друг друга:

  • useFormStatus — отвечает на вопрос «форма сейчас отправляется?». Используется в дочерних компонентах для UI-реакции.
  • useActionState (ранее useFormState) — управляет состоянием, которое возвращает Server Action. Используется в компоненте формы для работы с результатами.
'use client';

import { useActionState } from 'react';
import { useFormStatus } from 'react-dom';

// useFormStatus — для дочернего компонента (статус отправки)
function SubmitButton() {
  const { pending } = useFormStatus();
  return <button type="submit" disabled={pending}>Отправить</button>;
}

// useActionState — для компонента формы (результат действия)
function MyForm() {
  const [state, action] = useActionState(serverAction, null);

  return (
    <form action={action}>
      <input name="data" />
      {state?.error && <p>{state.error}</p>}
      <SubmitButton /> {/* Использует useFormStatus */}
    </form>
  );
}

Доступность (Accessibility)

При использовании useFormStatus важно не забывать об атрибутах доступности.

import { useFormStatus } from 'react-dom';

function AccessibleSubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button
      type="submit"
      disabled={pending}
      aria-disabled={pending}
      aria-busy={pending}
      aria-label={pending ? 'Форма отправляется, подождите' : 'Отправить форму'}
    >
      {pending ? (
        <>
          <span aria-hidden="true" className="spinner" />
          <span>Отправка...</span>
        </>
      ) : (
        'Отправить'
      )}
    </button>
  );
}

Атрибут aria-busy сообщает скринридерам, что элемент обновляется. Это улучшает опыт пользователей со вспомогательными технологиями.

Лучшие практики

1. Выносите кнопку в отдельный компонент

// ✅ Хорошо — кнопка переиспользуемая
function SubmitButton({ children }: { children: React.ReactNode }) {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Загрузка...' : children}
    </button>
  );
}

// ❌ Плохо — логика смешана с формой
function Form() {
  // Здесь useFormStatus не работает корректно
  return <form action={action}><button type="submit">Отправить</button></form>;
}

2. Используйте семантические атрибуты

Всегда добавляйте disabled и aria-disabled одновременно: disabled блокирует кнопку, а aria-disabled информирует вспомогательные технологии.

3. Показывайте визуальный фидбек

Пользователь должен всегда видеть, что происходит. Используйте спиннеры, анимации или смену текста.

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending} className="btn">
      {pending && <Spinner className="btn-spinner" />}
      <span>{pending ? 'Сохраняем...' : 'Сохранить'}</span>
    </button>
  );
}

4. Не злоупотребляйте полем data

Поле data содержит все данные формы. Не показывайте чувствительные данные (пароли, токены) в UI.

function FormStatus() {
  const { pending, data } = useFormStatus();

  if (!pending || !data) return null;

  // ✅ Безопасно — показываем только email
  const email = data.get('email');

  // ❌ Небезопасно — не показывайте пароли!
  // const password = data.get('password');

  return <p>Отправляем на {email as string}...</p>;
}

5. Комбинируйте с useActionState для полного контроля

useFormStatus + useActionState = полноценная обработка форм с загрузкой, результатами и ошибками.

Требования и совместимость

ТребованиеВерсия
React19.0+
react-dom19.0+
Next.js14+ (с App Router)
Импортimport { useFormStatus } from 'react-dom'

Примечание: useFormStatus появился в React 19. В React 18 он не работает. Для Next.js 13 с App Router в экспериментальном режиме была ограниченная поддержка, но стабильной она стала только с React 19.

Частые вопросы

Почему pending всегда false в моём компоненте?

Убедитесь, что компонент с useFormStatus является дочерним элементом <form>, а не самой формой. Хук не работает в компоненте, который рендерит саму форму.

Работает ли useFormStatus с обычным onSubmit?

useFormStatus работает только с формами, использующими нативный атрибут action (строка URL или Server Action). С onSubmit хук не обновляет pending.

Можно ли использовать useFormStatus в серверном компоненте?

Нет. useFormStatus — клиентский хук. Компонент, его использующий, должен иметь директиву 'use client'.

Что происходит при ошибке в Server Action?

Если Server Action выбрасывает ошибку, pending автоматически сбросится в false. Обрабатывайте ошибки через useActionState или try/catch внутри действия.

Итог

useFormStatus — это элегантное решение для отслеживания статуса отправки формы в React 19. Хук позволяет:

  • Убрать prop drilling — дочерние компоненты сами получают нужный статус.
  • Создавать переиспользуемые компоненты — универсальные кнопки, поля, индикаторы загрузки.
  • Улучшать UX — пользователь всегда видит, что форма обрабатывается.
  • Легко интегрироваться с Server Actions — особенно в Next.js App Router.

Хук прост в использовании, но важно помнить главное правило: вызывайте его только в дочерних компонентах формы, а не в самом компоненте с <form>. В паре с useActionState он даёт полный контроль над жизненным циклом отправки форм.

Хотите углубиться в React и научиться строить современные формы? Загляните на курс по React на PurpleSchool — там вы найдёте практические примеры и разберёте все аспекты работы с современными хуками React.

Стрелочка влевоuseId — генерация уникальных идентификаторовuseDeferredValue — отложенное обновление состоянияСтрелочка вправо

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

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

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

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

Все гайды по React

React тестирование с Vitest
Zustand: продвинутые паттерны, persist middleware и slicesUncontrolled Components: когда DOM управляет даннымиRender Props: гибкое управление рендерингом в ReactБезопасность в React: защита от XSS, CSRF и утечек данныхРефакторинг React-кода: техники и лучшие практикиReact с WebSocket — реалтайм в компонентахReact Native New Architecture: Fabric и JSIReact Native приложение с нуля до публикации в сторахПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаИменование компонентов в React: соглашения и лучшие практикиЛенивая загрузка: как ускорить React-приложение в разыОшибки гидратации в React и способы их устраненияHOC в React: мастерство композиции компонентовАрхитектура больших React приложений: Feature-Sliced DesignuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиДокументирование компонентов в React: Storybook, JSDoc и READMECompound Components в React: создаем гибкие компоненты с мощным APIКомпозиция компонентов в React: строим гибкие интерфейсыКомментирование кода в React: когда и как писать комментарииCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с даннымиДоступность (a11y) в React: ARIA, семантика и клавиатурная навигация
Zustand — управление состоянием в ReactZod - валидация с TypeScriptYup - валидация схемXState - конечные автоматыТемизация в ReactТестирование хуковTailwind CSS с ReactSWR - библиотека для запросов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 Hook Form — валидация форм в ReactReact Icons - библиотека иконок для ReactReact Dropzone — загрузка файловПодключение Bootstrap к React-приложениюАнимация при монтировании компонентов в ReactReact Beautiful DnD - перетаскивание элементовМокирование APIMobX — реактивное управление состоянием в ReactМикрофронтенды с React (micro-frontends)Загрузка и индикаторыАнимация списков в ReactJotai - атомарное состояниеБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactCSSTransition - переходыE2E тестирование с CypressCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовОбработка ошибок API
Valtio: атомарный state management в ReactuseState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseLayoutEffect в React — эффект до отрисовкиuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseId — генерация уникальных идентификаторовuseFormStatus - отслеживание статуса отправки формыuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиuseMemo в React: как и когда оптимизировать тяжелые вычисленияХук use() в React 19useEffect в React что это и как использоватьuseContext — работа с контекстом в ReactuseCallback в React — мемоизация функций и оптимизация ре-рендеровuseActionState в React 19React Stale Closures — устаревшие замыкания в хукахОптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextНовые возможности React 19Как предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияflushSync — синхронные обновления состояния в ReactuseEffect 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 ₽
Подробнее

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