React 19 Actions: Simplifying Async Mutations and Form Handling

May 22, 2025 (1y ago)

React developers, rejoice! Form submissions and async mutations just got a major upgrade with React 19 Actions. This new pattern simplifies the boilerplate around pending states, error handling, and optimistic updates that we've been managing manually with useState and useEffect for years.

Goodbye, Manual Pending State Management:

For years, handling a form submission in React meant juggling multiple pieces of state: a pending boolean, an error string, and the data itself. Libraries like React Query and SWR helped with data fetching, but mutations still required careful orchestration. React 19 Actions bake this pattern into the framework with the useActionState hook and the form action prop.

Link to section: The ,[object Object], HookThe useActionState Hook

The useActionState hook (formerly useFormState) pairs a function with the state it produces, automatically tracking the pending state:

import { useActionState } from 'react';

async function submitForm(prevState, formData) {
  try {
    const response = await fetch('/api/submit', {
      method: 'POST',
      body: formData,
    });
    if (!response.ok) throw new Error('Submission failed');
    return { success: true, message: 'Saved successfully!' };
  } catch (error) {
    return { success: false, message: error.message };
  }
}

function ContactForm() {
  const [state, formAction, isPending] = useActionState(submitForm, {
    success: false,
    message: '',
  });

  return (
    <form action={formAction}>
      <input name="email" type="email" required />
      <button type="submit" disabled={isPending}>
        {isPending ? 'Submitting...' : 'Submit'}
      </button>
      {state.message && (
        <p style={{ color: state.success ? 'green' : 'red' }}>
          {state.message}
        </p>
      )}
    </form>
  );
}

Notice what's gone: no useState for isPending, no useEffect to reset it, no manual try/catch in the component body. The isPending flag is derived automatically from the action's execution.

Link to section: The ,[object Object], HookThe useOptimistic Hook

For instant UI feedback, pair Actions with the useOptimistic hook to show the expected result before the server responds:

import { useOptimistic } from 'react';

function TodoList({ todos, sendTodo }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo) => [...state, { ...newTodo, pending: true }]
  );

  async function addTodo(formData) {
    const title = formData.get('title');
    addOptimisticTodo({ id: 'temp', title });
    await sendTodo(title);
  }

  return (
    <ul>
      {optimisticTodos.map((todo) => (
        <li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
          {todo.title}
        </li>
      ))}
    </ul>
  );
}

The optimistic state is shown immediately, then reconciled with the real data once the action completes.

Link to section: Server Actions IntegrationServer Actions Integration

Actions shine brightest with Server Components. A Server Action runs on the server without writing an API route:

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

export async function createPost(formData: FormData) {
  const title = formData.get('title');
  await db.post.create({ data: { title } });
  revalidatePath('/');
}
// app/page.tsx
import { createPost } from './actions';

export default function Page() {
  return (
    <form action={createPost}>
      <input name="title" />
      <button type="submit">Create</button>
    </form>
  );
}

No onSubmit handler, no fetch call, no JSON parsing: the form posts directly to the server action.

Embrace a New Era of Form Handling:

React 19 Actions mark a significant shift in how we handle mutations, reducing boilerplate and making pending states declarative. With useActionState, useOptimistic, and Server Actions, this pattern is set to streamline data mutations across your React applications.

React Docs: useActionState

Start exploring React 19 Actions and simplify your form and mutation handling today!