Level up with Forms Management with Next.js App Router

    I respect your privacy. Unsubscribe at any time.

    This is a free tutorial

    In exchange for your email address, you'll get full access to this and other free ProNextJS tutorials.

    Why? First and foremost, your inbox allows us to directly communicate about the latest ProNextJS material. This includes free tutorials, NextJS Tips, and periodic update about trends, tools, and NextJS happenings that I'm excited about.

    In addition to the piles of free NextJS content, you'll get the earliest access and best discounts to the paid courses when they launch.

    There won't be any spam, and every email you get will have an unsubscribe link.

    If this sounds like a fair trade, let's go!


    Implement a Server Action for Handling Form Data

    Jack HerringtonJack Herrington

    We've seen three ways to handle verification: two involving sending data to an API endpoint, and one using a server action to post data to the server.

    Now we'll look at our fourth method: using a server action that posts form data to the server instead of just using a schema. This approach has an advantage the the others don't.

    Setting up Form Data Action

    Start by creating a new onFormAction function inside of page.tsx.

    We can simply copy and paste the onDataAction we used last time and modify it slightly. This time the the function will accept formData argument that is typed as FormData, and the parsing steps will be updated accordingly:

    const onFormAction = async (formData: FormData) => {
      "use server";
      const data = Object.fromEntries(formData);
      const parsed = schema.safeParse(data);
      // conditional logic below

    With that change in place, we need to update the RegistrationForm component.

    Updating RegistrationForm

    Over in RegistrationForm.tsx, comment out the previous implementation.

    Then we'll replace the data variable with formData and formAction, then replace the data action with formAction to handle our server data postings:

    // inside of RegistrationForm
    const onSubmit = async (data: z.infer<typeof schema>) => {
      // commented out fetch implementations above
      const formData = new FormData();
      formData.append("first", data.first);
      formData.append("last", data.last);
      console.log(await formAction(formData));

    With this change, we can test the form in our browser and it works as expected.

    However, there's an easier way to do this that has the added benefit of working even when JavaScript isn't enabled.

    Add Form State to the Registration Form

    Back inside of RegistrationForm.tsx, import useFormState from react-dom. This will allow us to have React manage our form state:

    import { useFormState } from "react-dom";

    The useFormState hook takes onFormAction as the first argument and the initial state as the second argument. In our case, we'll set it to an object with a message property set to nothing. We'll then destructure the current form state and the formAction from the hook:

    // inside of RegistrationForm
    const [state, formAction] = useFormState(onFormAction, {
      message: ""

    We can then use this state and formAction inside of the form.

    Updating the Rendered Form

    Inside the <Form> component returned from the RegistrationForm, we'll add a div to conditionally display the message from state, and add the formAction to the form's action attribute:

    // inside of RegistrationForm
    return (
      <Form {...form}>

    For the time being, comment out the implementation of the onSubmit to test the form without validation.

    When hitting the submit button, we'll see the "Invalid data" message appear which is coming from the server.

    However, we want to have the client-side validation back.

    Handling Form Submissions with useRef

    In order to handle the form validation, we'll use React's useRef hook to get a reference to the form. First, import useRef:

    import { useRef } from "react";

    Next, create a new formRef inside of the RegistrationForm component and set it to the HTMLFormElement reference:

    // Above the return inside of RegistrationForm
    const formRef = useRef<HTMLFormElement>(null);

    Then inside of the form, we'll add the ref pointing to formRef and update the onSubmit prop. Instead of calling onSubmit directly, we can give it a function that will use the formRef to call the actual submit function on the form:

      onSubmit={form.handleSubmit(() => formRef?.current?.submit())}

    Now when we hit the submit button, the legit submit action on the form will be called, which will trigger our formAction and send our data.

    Final Testing

    After making this change, we'll test our form. Hitting submit works just like we want it to. We've still got our client-side validation, but now our server-side validation kicks in when the client-side validation doesn't pick up something. We get the "User registered" message and everything works as expected.

    What's more, even if you disable JavaScript, the form submission and server-side validation still work perfectly!

    the form works as expected

    That's the importance of both client-side and server-side validations– they ensure data integrity and security in seamless user experience, even when JavaScript is disabled.

    In the next video, we'll dive into something even more interesting: server-side field validation. Stay tuned!


    So far, we've got two different ways to send data to an API endpoint and validate it. And we've taken a look at one server action way to post data to the server. I'm going to give you a second server action way, and I think probably save the best for last. All right,

    so here's what we're going to do. Instead of sending the schema, like we did the last time with the data, with the first name, email, and last name, we're going to send form data to the server. And this has an advantage that the others don't. All right, let's get into it. So the first thing we're going to do is I'm going to go into page and I'm going

    to create a new form action. So I'll just copy and paste data action and change the form action. And instead of the schema, we'll instead do form data and we'll call that form data. And again, we need that object from entries trick to convert form data into data.

    And yeah, that, that's it. So far. So let's go send that onto our registration form component. And now we've got to go and alter registration form. So we get that. So we'll just copy paste this

    and we'll copy paste this and we'll use form data here. Now down here, let's go and copy and paste this out. And then we'll comment that.

    So now I have our different methodologies. This one, we need a form data. So we're just going to go and copy paste like that. And that'll give us form data. Instead of data, we use form action instead. Okay. So sure. That's one way to do it. Let's go to try and see what works.

    Okay. We'll open up a console, hit submit. And there we go. User registered. All right. Now you could do it this way, but there's an easier way to do it.

    And it has the advantage that it works without JavaScript being enabled. It's super cool. Let's go back into our code. And I'm going to bring in use form state from react dom. Okay. And that's going to manage our form state. So let's go and use that.

    So use form state takes the form action as the first argument, and then it takes the initial state. In this case, I'm just going to say that that's going to be message and then nothing. And this gives us back a state and a form action.

    Now the red squiggly issue with on form action is that it's API is wrong. First thing you get is a previous state and that matches whatever the state is that's coming out of the other side.

    So now let's go over into our form and let's make that the API. All right. Looking good. So now we're going to take that form action and down here in our form,

    we want to add an action and set that to form action. And then above the form, we're going to display the message. If this message exists now, we're not going to do the submit in here. We're just going to let the action do the submit. All right, let's try it.

    All right. So our validation is still working. And we hit submit and nothing happens. So let's remove for the moment, the validation by getting rid of the on submit. Now let's give it a go. Oh, interesting. Okay. So now we get, you hit submit and we get invalid data.

    That's actually coming from the server. Awesome. Because our server action over here gives us a message back of invalid data if it doesn't actually pass our validation. So this is really cool, but we want the validation back. So how are we going to do that validation? Well, what we can do is we can get a reference to this form.

    So bring in use ref from React. We'll create a ref for the form. We'll set that reference. And then in our on submit,

    instead of calling on submit, we can simply give it a function that says that we will call on form ref current submit. So if handle submit works, it's going to call this function.

    That function is then going to call the legit submit on form, which is going to trigger our form action, which is going to send our data. Let's see if it works. All right. So let's hit submit again. Now we get our validation. Let's go and complete our validation.

    Hit submit. And there we go. User registered. And that works just fine, but even cooler. Let's go and disable JavaScript. Hit refresh again. And now we hit submit and no,

    we don't get the client side validation because that all depends on JavaScript, but we do get the server side validation. So remember I said the client and server side validation are really important. Here it is. It's not just about data integrity and security. It's also, if you have JavaScript disabled like this,

    you can use this whole mechanism and still and still get invalidation and the whole thing. It's fantastic. In the next video, I'm going to show you something that is possibly arguably even cooler. It's server side field validation. I'll see you in the next video.