FrontendPrep
Back to React Questions
reactMedium

React 19 Actions and useActionState

Learn how React 19 simplifies form handling, pending states, and error handling natively using Actions and the new useActionState hook.

React 19 Actions and useActionState

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:

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:

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.

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>
  );
}

[!IMPORTANT] 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.

Share this Resource

Help other developers level up by sharing this study guide.

More Technical Questions

Expand your mastery. Deep dive into other frontend interview challenges in this category.