In previous versions of React, handling form submissions, loading indicators, and error states required writing a lot of repetitive boilerplate code. React 19 introduces Actions and the new useActionState hook to make managing these operations much simpler and cleaner.


The Problem: The Pre-React 19 Boilerplate#

Before React 19, if you wanted to submit a form asynchronously, you had to manually track the loading (isPending) and error states using multiple useState hooks.

Here is a common implementation:

TSX
import { useState } from "react";

function UpdateName() {
  const [name, setName] = useState("");
  const [isPending, setIsPending] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setIsPending(true);
    setError(null);

    try {
      await updateNameApi(name);
      alert("Name updated successfully!");
    } catch (err: any) {
      setError(err.message || "Something went wrong");
    } finally {
      setIsPending(false);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input 
        value={name} 
        onChange={(e) => setName(e.target.value)} 
        disabled={isPending} 
      />
      <button type="submit" disabled={isPending}>
        {isPending ? "Updating..." : "Update"}
      </button>
      {error && <p style={{ color: "red" }}>{error}</p>}
    </form>
  );
}

This approach works, but it requires manually toggling loading states and wrapping logic inside try/catch/finally blocks for every form.


The Solution: React 19 Actions#

In React 19, any function that performs an asynchronous transition is referred to as an Action. When an Action is triggered, React automatically manages the lifecycle of the transition:

  1. Auto-Pending State: React tracks when the asynchronous function starts and ends, providing the pending state automatically.
  2. Auto-Optimistic Updates: React can temporarily update the UI before the API call finishes.
  3. Form Integration: React natively supports passing async functions directly to the HTML <form action> attribute.

Form Handling with useActionState#

The useActionState hook (previously named useFormState in canary versions) is the primary way to manage Actions. It accepts an async handler and returns the current state, a dispatch function to trigger the action, and a pending boolean.

Here is the same form refactored using React 19's useActionState:

TSX
import { useActionState } from "react";

// The Action function: receives previous state and the form data
async function updateNameAction(prevState: any, formData: FormData) {
  const name = formData.get("username") as string;
  try {
    await updateNameApi(name);
    return { success: true, error: null };
  } catch (err: any) {
    return { success: false, error: err.message || "Failed to update name" };
  }
}

function UpdateName() {
  // useActionState takes: (actionFunction, initialActionState)
  // It returns: [currentState, formActionDispatcher, isPending]
  const [state, formAction, isPending] = useActionState(updateNameAction, {
    success: false,
    error: null,
  });

  return (
    <form action={formAction}>
      <input 
        name="username" 
        type="text" 
        placeholder="Enter new name" 
        disabled={isPending} 
      />
      <button type="submit" disabled={isPending}>
        {isPending ? "Updating..." : "Update"}
      </button>
      
      {state.error && <p style={{ color: "red" }}>{state.error}</p>}
      {state.success && <p style={{ color: "green" }}>Name updated!</p>}
    </form>
  );
}

Key Differences & Improvements:#

  • No manual useState: We no longer manually track isPending or error.
  • Standard HTML FormData: We pass formAction directly to the <form action="..."> attribute. React retrieves values using the standard Web API FormData via form input name attributes.
  • No onSubmit or preventDefault(): React automatically intercepts the form submission and prevents the default page reload, meaning we do not write event handlers.

How to Handle Pending States in Child Components?#

If you have button elements nested deep inside a form, passing down the isPending boolean as a prop can be tedious. React 19 provides the useFormStatus hook to read the parent form's status from any child component.

TSX
import { useFormStatus } from "react-dom";

function SubmitButton() {
  // useFormStatus returns status from the nearest ancestor <form>
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? "Saving..." : "Save Changes"}
    </button>
  );
}

Note

useFormStatus will only work if the component is rendered inside a <form> tag. It will not work if declared in the same component that renders the <form> wrapper itself.


Key Takeaways#

  • Actions are functions that handle asynchronous state transitions (like API calls) automatically.
  • useActionState manages the returned value of an action and provides the loading state (isPending) without manual trackers.
  • <form action> supports passing React Actions directly, replacing onSubmit and automatic event cancellation boilerplate.
  • useFormStatus reads the parent form's pending state from nested child components without prop-drilling.