React 19 introduces Actions, which are asynchronous functions. Actions are helpful in making form submissions easier. This blog post dives into what Actions are and how to use them.
In this article, we are going to learn about:
The new React 19 feature - Actions
The new React 19 hooks - useActionState and useFormStatus
Converting a React 18 form to a React 19 form
Feature: React Actions
To understand Actions, let's first take a look at how we manage forms today. In React 18 and earlier, we submit forms using the
handleSubmit
function in a button. Here's a simple form that has one input fieldname
:
// Form submission in React 18
console.info('React 18 form');
const [name, setName] = useState('');
const [isPending, setIsPending] = useState(false);
const handleChange = (event) => {
setName(event.target.value);
};
const handleSubmit = (event) => {
event.preventDefault();
setIsPending(true);
setTimeout(() => {
// call API
setIsPending(false);
}, 500);
};
return (
<form>
<input type="text" name="name" onChange={handleChange} />
{isPending ? <p>Loading...</p> : <p>Hello in React 18, {name}</p>}
<button onClick={handleSubmit} disabled={isPending}>
Update
</button>
</form>
);
In this code, we are doing the following:
Adding a Loading State: We use a variable isPending to manually keep track of the loading state.
Form Submission: The form is submitted using the handleSubmit event handler attached to the onClick event of the button.
Capturing Submitted Value: The handleChange function captures the submitted value and stores it in state variables.
Actions
With React 19, handling forms becomes easier with Actions, inspired by frameworks such as Remix. One key feature is the enhanced use of startTransition to manage pending states.
startTransition was introduced in React 18, allowing developers to mark certain updates as less urgent. In React 19, startTransition can now handle async functions, making it even more powerful for managing asynchronous tasks and improving the user experience during form submissions.
const [isPending, startTransition] = useTransition();
const handleSubmit = () => {
startTransition(async () => {
const error = await updateName(name);
if (error) {
setError(error);
return;
}
redirect('/path');
});
};
This async function inside startTransition
is called Action. What makes them cool is that actions can be used directly to submit forms like so -
<form action="{actionFn}">...</form>
This format may look familiar if you are experienced with PHP.
How to create an action?
To create an async function, we can use a new hook introduced in React 19 - useActionState
. We call this hook and pass in an action function and an initial state. This hook returns the updated state and a form action actionFn
, which can be used to wire up a form.
const [state, actionFn] = useActionState(submitAction, { name: '' });
Now with this wired up with the form, we have -
<form action={actionFn}>
<input type="text" name="name" />
<button type="submit" disabled="{pending}">
Update
</button>
</form>
To add a loading state, we can use a new hook introduced in React 19 called useFormStatus
.
const { pending, data, method, action } = useFormStatus();
This hook provides information on the status of the form. The pending
state indicates whether the form is being submitted, and data
is a FormData
object containing the submitted data. We use this pending state to show a loader. However, there is one caveat - this hook can only be used in a child component, not in the form itself. So, we have to create child components SubmitButton and Loader to retrieve pending
state:
function Loader() {
const { pending } = useFormStatus();
return <div>{pending && "Loading..."}</div>;
}
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
Update
</button>
);
}
....
return(
<form action={formAction}>
<input type="text" name="name" />
<Loader />
<SubmitButton />
</form>
)
We can also capture useful information about the data submitted to the form by retrieving it from the state returned from useActionState
.
const [state, formAction] = useActionState(submitAction, { name: '' });
So here's the final form -
function Loader() {
const { pending } = useFormStatus();
return <div>{pending && 'Loading...'}</div>;
}
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
Update
</button>
);
}
function Name({ name }) {
return <p>Hello in 19 {name}</p>;
}
function App() {
console.info('React 19 form');
const [state, formAction] = useActionState(submitAction, { name: '' });
return (
<form action={formAction}>
<input type="text" name="name" />
<Loader />
<SubmitButton />
<Name name={state?.name} />
</form>
);
}
Compare this with React 18 form at the top of this post to check the difference.
By utilizing actions along with hooks like useActionState
and useFormStatus
, we can easily manage form states, capture submitted data, and provide responsive feedback to users during form submissions to show pending states. I am excited for this improved experience of handling forms in React 19, and I look forward to removing unnecessary handleSubmits
, useState
s and pending
state.
In my next article, I will discuss an exciting new React feature - the React Compiler. This tool automatically memoizes, eliminating the need for useMemo and useCallback. Stay updated..................