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:
- Auto-Pending State: React tracks when the asynchronous function starts and ends, providing the pending state automatically.
- Auto-Optimistic Updates: React can temporarily update the UI before the API call finishes.
- 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 trackisPendingorerror. - Standard HTML
FormData: We passformActiondirectly to the<form action="...">attribute. React retrieves values using the standard Web APIFormDatavia form inputnameattributes. - No
onSubmitorpreventDefault(): 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]
useFormStatuswill 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.
useActionStatemanages the returned value of an action and provides the loading state (isPending) without manual trackers.<form action>supports passing React Actions directly, replacingonSubmitand automatic event cancellation boilerplate.useFormStatusreads the parent form's pending state from nested child components without prop-drilling.