Submitting a form in Nextjs 14 with onBlur, server actions, and validation
Tags: webdev, nextjs, frontend, typescript
Published: 2024-02-07 19:10:21

Want a straightforward article on submitting a form in Nextjs to a server action using an HTML event `onBlur`? And with form validation?

Then this is the article for you. I found the documentation on Nextjs.org lacking, so I'm putting this here for you.

This article is suitable for other events, such as `onChange`. Still, I prefer `onBlur`; as you know, the user is done typing, and you don't have to mess with debouncing.

## The Form
This tutorial has a simple `input` field within the `form` asking for a name. 

I want the user to save the field without hitting a submit button. So, the form has no buttons; the user types and the code does the rest.

```
"use client";

import React from "react";
import { useFormState } from "react-dom";
import { saveName } from "@/src/libs/actions/SaveName";

export default function NameForm() {

  const initialState = { message: "", errors: {} };
  const [state, formAction] = useFormState(saveName, initialState);

  return (
    <form action={formAction}>
      {state && state.message && <div>{state.message}</div>}
      <div>
        <label htmlFor="name">
          Enter your name
        </label>
        <div>
          <input
              name="name" 
              id="name"   
              type="text"
              required
              placeholder="Ex. Brandon Sanderson"
              onBlur={async (e) => { 
                const formData = new FormData();
                formData.append("name", e.target.value);
                await formAction(formData); 
              }}
          />
          {state && state.errors?.name &&
            name((error: string) => (
              <p key={error}>
                {error}
              </p>
            ))}
        </div>
      </div>
    </form>
  );
};
```

Okay, let's break this down. To make our lives easier and provide error messages, we're using the [React hook](https://react.dev/reference/react-dom/hooks/useFormState) `useFormState`. As of this writing, this newer feature is only available in a Canary build. But it's been stable for me.

```
const initialState = { message: "", errors: {} };
const [state, formAction] = useFormState(saveName, initialState);
```

We first create a state object, which we'll use to handle our validation messages.

We then create our state object and formAction by passing to useFormState a reference to our function (a [NextJS server action](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations)) and the initial state.

```
<form action={formAction}>
```

We use the Nextjs syntax to call our server action in our form. If you don't include this when a user hits enter within a field, it will not call your server action.

```
<input
  name="name" 
  id="name"   
  type="text"
  required
  placeholder="Ex. Brandon Sanderson"
  onBlur={async (e) => { 
    const formData = new FormData();
    formData.append("name", e.target.value);
    await formAction(formData); 
  }}
/>
```

The `input` is standard right up to the `onBlur`. We cannot call the form's `action` from `onBlur`, but we can trigger the same action ourselves. But you must make a `FormData` object yourself. `FormData` is just a simple interface; you can create it with a call to `new FormData()`. We then populate it with an `append`, which requires a key/value pair. Then, we call our server action created via the hook.


## The Server Action
For the server action, we'll include validation since we want to give the user an experience that lets them know if something has gone wrong.

```
"use server";

import { z } from 'zod';

const NameFormSchema = z.object({
    name: z.string().min(2, { message: 'You must enter at least two characters'}),
});

export type State = {
    errors?: {
        name?: string[];
    };
    message?: string | null;
}

export async function saveWebsite (prevState: State | undefined, formData: FormData) {
    
    // Validate Inputs
    const validatedFields = WebsiteFormSchema.safeParse({
        name: formData.get('name'),
    });

    // Handle Validation Errors
	if (!validatedFields.success) {
        const state: State = {
            errors: validatedFields.error.flatten().fieldErrors,
            message: 'Oops, I think there\'s a mistake with your inputs.',
        }

        return state;
	}
	
	// Do your save here...
}
```

I'm a big fan of [Zod](https://zod.dev), a typescript validation library. You could roll this yourself. But why do life in hard mode?

Let's break this code down

```
const NameFormSchema = z.object({
    name: z.string().min(2, { message: 'You must enter at least two characters'}),
});
```

Zod uses a schema format. So first, you need to tell it the schema of the form you'll be using. Each field you're submitting will have an entry. In this case, just one field.

```
export type State = {
    errors?: {
        name?: string[];
    };
    message?: string | null;
}
```

This is the type we'll be using for our form state. We must define its type fully because we're populating it and using TypeScript. We use this object to pass back errors. If you look at the form, you can see how it's used, as it's fairly straightforward.

```
// Validate Inputs
    const validatedFields = WebsiteFormSchema.safeParse({
        name: formData.get('name'),
    });

    // Handle Validation Errors
	if (!validatedFields.success) {
        const state: State = {
            errors: validatedFields.error.flatten().fieldErrors,
            message: 'Oops, I think there\'s a mistake with your inputs.',
        }

        return state;
	}
```

Validation is also straightforward with Zod. First, we safely parse the fields we want. Then, we check for any errors that were created during the parse. If they exist, we package them up into our state object.

The last step is up to you. Save to a database or call an API. But be sure to capture any errors and use the state object if they occur.