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