Form Actions with the useFormState Hook
React 19 is coming out and with it a new compiler as well as support for form actions, and cool new form handling hooks like useFormState
. If you’ve been working with the NextJS App Router you can have access to these awesome new tools today!
Let me walk you through the basics of this new useFormState
hook and its interplay with form actions.
Form Actions
First let’s figure out how to use form actions. We will need two things to start, a form, to post to the form action, and the server action that will receive the data from the form, process it and return the result.
Let’s start on the server side first. To create a server action we create another module in our application, for example formPostAction.ts
and in that file we define a server action like so:
"use server";
type FormState = {
message: string;
}
export async function onFormPostAction(prevState: FormState, data: FormData) {
// Process the data
return {
message: "Form data processed";
}
}
The server action function onFormPostAction
needs to be defined as an async
function. It also need to either have "use server"
as the first line of the function, or "use server"
needs to be at the top of the module file, as it is here. In the second case it means that all of the functions in the file are server only functions.
To use the useFormState
hook on the client your action also needs to have a specific signature. With the previous state being the first argument, and the form data as the second argument. The shape of the form state is up to you, ours just has a message
in it. Both the previous state and the return from the post action function should be the same type.
Now let’s try this out on the client.
useFormState On The Client
To use our form action on the client we need to import that action function from formPostAction.ts
and send it to the useFormState
function from react-dom
, like so:
"use client";
import { useFormState } from "react-dom";
import { onFormPostAction } from "./formPostAction.ts";
export default function MyForm() {
const [state, action] = useFormState(onFormPostAction, {
message: "",
});
// ...
}
Right at the top of this file we can see that this is going to be a client component because there is a "use client";
directive. We need that so that we can use the useFormState
hook.
Then in the body of the component we invoke useFormState
and give it two things; the server action function, and the initial state. The initial state object needs to match the type of the FormState
type in formPostAction.ts
.
What comes out is a tuple with the current state and an action function. On the initial render that state
value will match the initial state. But after a form post the state
will be whatever came back from the server.
The action
function is what we send to the form tag. Like so:
export default function MyForm() {
const [state, action] = useFormState(...);
const [first, setFirst] = useState("");
return (
<form action={action}>
<input
type="text" name="first"
value={first} onChange={(e) => setFirst(e.target.value)}
/>
<button type="submit">Submit</button>
</form>
);
}
Now we’ve added a form
tag that takes has the action
property defined with the action
function returned from useFormState
. It also has an input
field for a first name that use standard useState
state, as well as a submit
button.
This is enough to try out this system. On first render we get a blank first name field that we can then populate. Hitting the submit button we send that data to the server action as the form data, and we get back the returned message as state
.
We can display that state easily by simply adding it to the JSX.
return (
<form action={action}>
<div>{state.message}</div>
...
</form>
);
And this will first show a blank div from the initial state, and then after a post we’ll display whatever the server sends back.
Remember that you can send back whatever data you want, and you can use that returned state anywhere in the component as you please.
useFormState’s Hidden Awesomeness
This is already a pretty slick mechanism for handling forms, but there is a hidden gem of a feature if you use it properly; it works without JavaScript enabled! That’s right! Try it for yourself, disable JavaScript in your browser and try it again. It will work without it. Which is a pretty novel thing in the world of React, let me tell you!
What’s Next
Of course, handing forms properly means validating the data before you send it to the server. And integrating a client validation library like React Hook Form can be tricky because you will want to run the validation before the action
is invoked. To learn amore about that you can check out the tutorial on form management (which not only shows this mechanism but three other ways to post data to the server as well). It also shows how to use React Hook Form in conjunction with useFormState
.
There is also a YouTube video on the Blue Collar Coder channel where we go even further to use the state
value returned from useFormState
to avoid data loss after an unsuccessful post.
Conclusion
Form actions and the useFormState
hook are just one more way that React is keeping pace (and often going beyond) the other more cutting edge frameworks.