Skip to main content
Ganesh Joshi
Back to Blogs

React 19 useFormStatus: track form submission state

February 22, 20265 min read
Tutorials
React form with useFormStatus and submission state on screen

Forms need feedback. When a user clicks submit, they need to know something is happening. React 19's useFormStatus hook provides the pending state of a form submission, making it easy to disable buttons, show spinners, and prevent double submissions.

What useFormStatus provides

The hook returns an object with submission details:

Property Type Description
pending boolean True while form is submitting
data FormData | null Form data being submitted
method string HTTP method (get, post, etc.)
action function The action being invoked

Most commonly, you use pending to control UI state.

Basic usage

Create a submit button component that uses the hook:

'use client';

import { useFormStatus } from 'react-dom';

export function SubmitButton({ children }: { children: React.ReactNode }) {
  const { pending } = useFormStatus();

  return (
    <button
      type="submit"
      disabled={pending}
      className={pending ? 'opacity-50 cursor-not-allowed' : ''}
    >
      {pending ? 'Submitting...' : children}
    </button>
  );
}

Use it inside a form:

import { submitForm } from './actions';
import { SubmitButton } from './SubmitButton';

export function ContactForm() {
  return (
    <form action={submitForm}>
      <input name="email" type="email" required />
      <input name="message" required />
      <SubmitButton>Send Message</SubmitButton>
    </form>
  );
}

When the form submits, the button automatically disables and shows "Submitting...".

The component boundary rule

useFormStatus must be called inside a child component of the form. It won't work at the form level:

// WRONG: useFormStatus at form level
function ContactForm() {
  const { pending } = useFormStatus(); // Won't work!

  return (
    <form action={submitForm}>
      <button disabled={pending}>Submit</button>
    </form>
  );
}

// CORRECT: useFormStatus in child component
function ContactForm() {
  return (
    <form action={submitForm}>
      <SubmitButton>Submit</SubmitButton> {/* useFormStatus inside */}
    </form>
  );
}

This design allows multiple forms on a page—each component only sees its parent form's status.

With Server Actions

useFormStatus integrates seamlessly with Next.js Server Actions:

// actions.ts
'use server';

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

  await db.insert(posts).values({ title, content });
  revalidatePath('/posts');
}

// page.tsx
import { createPost } from './actions';
import { SubmitButton } from '@/components/SubmitButton';

export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Title" required />
      <textarea name="content" placeholder="Content" required />
      <SubmitButton>Create Post</SubmitButton>
    </form>
  );
}

Loading indicators

Show different loading states:

'use client';

import { useFormStatus } from 'react-dom';
import { Loader2 } from 'lucide-react';

export function SubmitButton({ children }: { children: React.ReactNode }) {
  const { pending } = useFormStatus();

  return (
    <button
      type="submit"
      disabled={pending}
      className="flex items-center gap-2"
    >
      {pending && <Loader2 className="h-4 w-4 animate-spin" />}
      {pending ? 'Saving...' : children}
    </button>
  );
}

Form-wide loading states

Disable all inputs during submission:

'use client';

import { useFormStatus } from 'react-dom';

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

  return (
    <fieldset disabled={pending} className={pending ? 'opacity-50' : ''}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <textarea name="message" placeholder="Message" required />
    </fieldset>
  );
}

Accessing submitted data

Use data to show what's being submitted:

'use client';

import { useFormStatus } from 'react-dom';

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

  if (!pending) return null;

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

  return (
    <div className="text-sm text-gray-500">
      Submitting for {email}...
    </div>
  );
}

Combining with useActionState

useFormStatus provides pending state. useActionState manages the action result:

'use client';

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

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Sending...' : 'Send'}
    </button>
  );
}

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

  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      <textarea name="message" required />

      {state.error && <p className="text-red-500">{state.error}</p>}
      {state.success && <p className="text-green-500">Sent!</p>}

      <SubmitButton />
    </form>
  );
}

Multiple submit buttons

Handle forms with multiple actions:

'use client';

import { useFormStatus } from 'react-dom';

export function ActionButtons() {
  const { pending, action } = useFormStatus();

  return (
    <div className="flex gap-2">
      <button
        type="submit"
        formAction={saveDraft}
        disabled={pending}
      >
        {pending && action === saveDraft ? 'Saving...' : 'Save Draft'}
      </button>
      <button
        type="submit"
        formAction={publish}
        disabled={pending}
      >
        {pending && action === publish ? 'Publishing...' : 'Publish'}
      </button>
    </div>
  );
}

Progressive enhancement

Forms work without JavaScript. useFormStatus enhances the experience:

// Server Component (no JavaScript needed)
export function BasicForm() {
  return (
    <form action={submitForm}>
      <input name="email" required />
      <button type="submit">Submit</button>
    </form>
  );
}

// Enhanced with client component
export function EnhancedForm() {
  return (
    <form action={submitForm}>
      <input name="email" required />
      <SubmitButton>Submit</SubmitButton> {/* Enhanced UX */}
    </form>
  );
}

Without JavaScript, the form still submits. With JavaScript, users get loading feedback.

Common patterns

Pattern Implementation
Disable on submit disabled={pending}
Show spinner {pending && <Spinner />}
Change button text {pending ? 'Saving...' : 'Save'}
Disable all fields <fieldset disabled={pending}>
Prevent double submit Button disabled + spinner
Show submitted data Access data.get('field')

Testing

Test both pending and completed states:

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('shows loading state on submit', async () => {
  render(<ContactForm />);

  await userEvent.type(screen.getByRole('textbox', { name: /email/i }), 'test@example.com');
  await userEvent.click(screen.getByRole('button', { name: /submit/i }));

  expect(screen.getByRole('button')).toBeDisabled();
  expect(screen.getByText(/submitting/i)).toBeInTheDocument();
});

Summary

useFormStatus provides the pending state of form submissions. Use it in child components to disable buttons, show spinners, and prevent double submissions. It works with Server Actions and client handlers alike. Combine with useActionState for complete form state management. The pattern improves UX while keeping forms progressively enhanced.

Frequently Asked Questions

useFormStatus is a React DOM hook that returns the pending state of a parent form. It tells you when a form is submitting so you can disable buttons, show spinners, or provide feedback.

useFormStatus must be used inside a component that is a child of a form element. It won't work at the form level itself or outside a form. Create a separate SubmitButton component.

Yes. When you pass a Server Action to a form's action prop, useFormStatus tracks the pending state of that action. It works with both Server Actions and client-side form handlers.

It returns an object with pending (boolean), data (FormData being submitted), method (form method), and action (the action function). Most commonly you use the pending property.

useFormStatus only provides pending state of the parent form. useActionState manages the full action lifecycle including return values, errors, and state updates. Use both together for complete form handling.

Related Posts