DEV Community

Cover image for Submitting a form in Nextjs 14 with onBlur, server actions, and validation
Bryan Smith
Bryan Smith

Posted on

Submitting a form in Nextjs 14 with onBlur, server actions, and validation

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

Okay, let's break this down. To make our lives easier and provide error messages, we're using the React hook 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);
Enter fullscreen mode Exit fullscreen mode

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) and the initial state.

<form action={formAction}>
Enter fullscreen mode Exit fullscreen mode

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); 
  }}
/>
Enter fullscreen mode Exit fullscreen mode

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...
}
Enter fullscreen mode Exit fullscreen mode

I'm a big fan of Zod, 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'}),
});
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (5)

Collapse
 
leandro_nnz profile image
Leandro Nuñez

Hi. Thanks for sharing. I have a couple of questions:

  • Why would you use onBlur instead of onChange? onBlur is going to be called every time you move away from the input.
  • Why would you execute the same logic as the form submission logic every time the onBlur happens?

Thanks!

Collapse
 
nathanburgess profile image
Nathan Burgess
  • onChange would call the formAction every single keystroke, which isn't very efficient. onBlur will persist the data once the change is complete.
  • That's the entire point of this post: to submit data onBlur, so that the users' data is saved without requiring them hitting a save button
Collapse
 
leandro_nnz profile image
Leandro Nuñez

I see.

  • What would be a use-case of this pattern?
  • what happens if the user changes the input value, navigates to other page before the async operation finished? Thanks in advance. Sorry for the trouble.
Thread Thread
 
orionseven profile image
Bryan Smith

I have a form where I need to capture the users interaction immediately. It's during signup and I don't want any save buttons and don't want any lost data. It can be inefficient, but it's a limited case and the payoff for less friction is worth it.

In my use case the user can't advance to another page, they're mid onboarding. But if that could be the case you'd need to handle the outcomes.

Collapse
 
truemanishtiwari profile image
Manish Tiwari

grtgrtg