ProNextJS

    Form Actions with the useFormState Hook

    Jack HerringtonJack Herrington

    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.

    forms management with next js app router

    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.

    Subscribe for Free Tips, Tutorials, and Special Discounts

    We're in this together!

    I respect your privacy. Unsubscribe at any time.