
React: useActionState and useFormStatus
Let's look at a few examples of how React 19's new useActionState and useFormStatus hooks work
React 19 introduces several interesting new features, including the useActionState hook. This experimental hook aims to simplify state and action management in React components, particularly for forms. In this article, we’ll explore useActionState by comparing it with the well-known useState hook, with code examples for both and a discussion of their pros and cons.
ℹ️ Note
to use these new hooks you need to switch to React 19, which is in beta. You can, for example, create a new project with vite with
npm create vite@latest
and then switch to the beta 19 versions of React with
npm i react@beta react-dom@beta
Comparing useActionState and useState
Let’s consider a simple login form with username and password fields.
Example with useState:
import React, { useState } from "react";
function App() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [response, setResponse] = useState(null);
const [isPending, setIsPending] = useState(false);
const [errors, setErrors] = useState(null);
const handleSubmit = async (event) => {
event.preventDefault();
// Set the states before sending the request
setResponse(null);
setIsPending(true);
setErrors(null);
// Send the form data to the server, simulating a response delay
await new Promise((resolve) => setTimeout(resolve, 2000));
// Set the states after the response based on what we receive
// obviously with real responses you'd need to check for errors
setResponse("done!");
setIsPending(false);
setErrors(null);
};
return (
<div>
<form onSubmit={handleSubmit}>
<label>Username:</label>
<input type="text" value={username} onChange={(e) => setUsername(e.target.value)} />
<label>Password:</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
<button type="submit">Log in</button>
</form>
<div>Response: {response}</div>
<div>Status: {isPending ? "loading..." : "ready"}</div>
</div>
);
}
export default App;
Example with useActionState:
import React, { useActionState } from "react";
function App() {
const sendRequest = async (prevState, formData) => {
const username = formData.get("username");
const password = formData.get("password");
// simulate sending data to the server with a delay
// here you could use a fetch request sending username and password
await new Promise((resolve) => setTimeout(resolve, 2000));
return { data: "done!", error: null };
// in case of an error you'd instead have something like
// return {data: prevState, error: 'Error!'}
};
const [userData, submitAction, isPending] = useActionState(sendRequest, {
data: null,
error: null,
});
return (
<div>
<form action={submitAction}>
<label>Username:</label>
<input type="text" name="username" />
<label>Password:</label>
<input type="password" name="password" />
<button type="submit">Log in</button>
</form>
<div>Response: {userData.data}</div>
<div>Status: {isPending ? "loading..." : "ready"}</div>
</div>
);
}
export default App;
What changes between the useState version and the useActionState version
Let’s look at how our code changes so we can understand the improvements.
- We no longer need to manage the state of the individual input fields, all that matters is giving each one the correct
name. So goodbyevalueandonChange=> a cleaner form; - We no longer have to manage the form’s states, these are already handled by the new hook, and we’ll see we also get additional benefits from using
useFormStatus; - Similarly, any errors generated by sending the data are exposed the same way.
The new useFormStatus hook
We also get another benefit. We can use the useFormStatus hook to read the form’s status and, for example, change the behavior of buttons or other elements in our form.
import { useFormStatus } from "react-dom"; // note: from `react-dom`, not `react`
const SubmitButton = () => {
const { pending } = useFormStatus();
return <button type="submit">{pending ? "Sending..." : "Submit"}</button>;
};
We can use this hook in any child element of the form, at any depth!
Let’s update the code to include this component too.
import React, { useActionState } from "react";
import { useFormStatus } from "react-dom";
const SubmitButton = () => {
const { pending } = useFormStatus();
return <button type="submit">{pending ? "Sending..." : "Submit"}</button>;
};
function App() {
const sendRequest = async (prevState, formData) => {
const username = formData.get("username");
const password = formData.get("password");
// simulate sending data to the server with a delay
// here you could use a fetch request sending username and password
await new Promise((resolve) => setTimeout(resolve, 2000));
return { data: "done!", error: null };
// in case of an error you'd instead have something like
// return {data: prevState, error: 'Error!'}
};
const [userData, submitAction, isPending] = useActionState(sendRequest, {
data: null,
error: null,
});
return (
<div>
<form action={submitAction}>
<label>Username:</label>
<input type="text" name="username" />
<label>Password:</label>
<input type="password" name="password" />
<SubmitButton />
</form>
<div>Response: {userData.data}</div>
<div>Status: {isPending ? "loading..." : "ready"}</div>
</div>
);
}
export default App;
Now when we click the “Submit” button, while the form is loading data, we’ll see “Sending…”. Once loading finishes, the button goes back to showing “Submit”. Nice, right? This hook also exposes other data we can use, again at any level!
const { pending, data, method, action } = useFormStatus();
As you can see we have all the information available that can help us manage our form well. And it lets us compose our components more cleanly and independently, without too many props to pass down.
Conclusion
In both examples, the form’s state is managed. However, useActionState offers several advantages:
- State and action integration:
useActionStatecombines the form’s state and related actions into a single structure, simplifying state management and reducing boilerplate code; - Automatic form submission handling:
useActionStateprovides a built-in submit action that automatically handles form submission and state updates based on the server’s response; - Less code: The
useActionStateexample is more concise and requires less code to manage the form’s state and actions;
The useActionState hook in React 19 represents a significant step forward in form management, offering a simpler and more cohesive interface for state and actions. Although it’s still an experimental feature, its potential to simplify React form development is clear.
We’re still in beta, but if you want to try it out just install the beta versions of react and react-dom as we saw in the introduction.
Happy coding 😉
References