Other

useActionState

Manages form action state with built-in pending tracking. Combines the action function pattern with automatic state management for form submissions (React 19+).

Signature

TypeScript
const [state, formAction, isPending] = useActionState<S>(action: (prevState: S, formData: FormData) => S | Promise<S>, initialState: S, permalink?: string)

Parameters

ParameterTypeDescription
action(prevState: S, formData: FormData) => S | Promise<S>A function called when the form is submitted. Receives the previous state and form data, returns the new state (or a Promise of it).
initialStateSThe initial state value before any form action has been dispatched.
permalinkstringOptional URL for progressive enhancement. Used when JavaScript hasn't loaded yet.

Return Value

A tuple of [state, formAction, isPending]. state is the current state returned by the action. formAction is a wrapped action to pass to form's action prop. isPending indicates if the action is in progress.

Examples

Form Submission
import { useActionState } from 'react';

interface FormState {
  message: string;
  errors: Record<string, string>;
}

async function submitForm(prev: FormState, formData: FormData): Promise<FormState> {
  const email = formData.get('email') as string;
  const name = formData.get('name') as string;

  if (!email.includes('@')) {
    return { message: '', errors: { email: 'Invalid email address' } };
  }

  await fetch('/api/subscribe', {
    method: 'POST',
    body: JSON.stringify({ email, name }),
  });

  return { message: 'Subscribed successfully!', errors: {} };
}

function SubscribeForm() {
  const [state, action, isPending] = useActionState(submitForm, {
    message: '',
    errors: {},
  });

  return (
    <form action={action}>
      <input name="name" placeholder="Name" />
      <input name="email" placeholder="Email" />
      {state.errors.email && <p style={{ color: 'red' }}>{state.errors.email}</p>}
      <button disabled={isPending}>{isPending ? 'Submitting...' : 'Subscribe'}</button>
      {state.message && <p style={{ color: 'green' }}>{state.message}</p>}
    </form>
  );
}
Server Action Integration
import { useActionState } from 'react';

// Server action (in a separate 'use server' file)
async function createTodo(prev: { todos: string[] }, formData: FormData) {
  'use server';
  const text = formData.get('text') as string;
  // Save to database...
  return { todos: [...prev.todos, text] };
}

function TodoForm() {
  const [state, action, isPending] = useActionState(createTodo, { todos: [] });

  return (
    <div>
      <form action={action}>
        <input name="text" placeholder="New todo..." />
        <button disabled={isPending}>Add</button>
      </form>
      <ul>
        {state.todos.map((t, i) => <li key={i}>{t}</li>)}
      </ul>
    </div>
  );
}
Multi-Step Form
import { useActionState } from 'react';

type Step = 'details' | 'confirm' | 'done';
interface WizardState { step: Step; data: Record<string, string>; error: string | null }

async function wizardAction(prev: WizardState, formData: FormData): Promise<WizardState> {
  const action = formData.get('_action') as string;

  if (action === 'next' && prev.step === 'details') {
    const name = formData.get('name') as string;
    if (!name) return { ...prev, error: 'Name is required' };
    return { step: 'confirm', data: { ...prev.data, name }, error: null };
  }

  if (action === 'submit' && prev.step === 'confirm') {
    await fetch('/api/register', { method: 'POST', body: JSON.stringify(prev.data) });
    return { step: 'done', data: prev.data, error: null };
  }

  if (action === 'back') {
    return { step: 'details', data: prev.data, error: null };
  }

  return prev;
}

function Wizard() {
  const [state, action, isPending] = useActionState(wizardAction, {
    step: 'details', data: {}, error: null,
  });
  // Render form steps based on state.step...
  return <form action={action}>...</form>;
}

Common Pitfalls

!

Confusing useActionState with useFormState — useActionState is the renamed and improved version in React 19.

!

Not handling errors inside the action — unhandled promise rejections won't update the state.

!

Forgetting that the action receives the previous state as the first argument, not just FormData.

!

Not providing a meaningful initialState, leading to undefined state on first render.

Understanding useActionState

useActionState is a React 19 hook that combines form action handling with automatic state management and pending tracking. It evolved from the experimental useFormState hook and provides a streamlined way to handle form submissions, including both client-side validation and server-side processing, without manually managing loading states or error handling.

The action function receives two arguments: the previous state and the FormData from the form submission. This design lets you build stateful forms where each submission can build on the results of previous ones — useful for multi-step wizards, accumulative forms, or workflows where server responses affect subsequent form behavior. The function can be synchronous or async, and React tracks the pending state automatically.

The formAction returned by useActionState is a wrapped version of your action that you pass to a form's action prop. This integration with HTML form semantics enables progressive enhancement — forms work even before JavaScript loads if you provide a permalink. When JavaScript is available, React intercepts the submission and handles it client-side with the full reactive experience.

The isPending flag simplifies one of the most common UI patterns: disabling the submit button and showing a loading indicator during submission. Unlike manual approaches that require separate useState calls for loading state, useActionState provides this for free. Combined with useOptimistic for instant UI updates and useFormStatus for nested component awareness, useActionState forms the core of React 19's form handling story.

Related Hooks

More Other Hooks

Explore All React Hooks

Browse our complete reference of 19 React hooks with signatures, examples, pitfalls, and in-depth explanations.