How to Create and Validate Forms With React Hook Form

React Hook Form (RHF) is a library for managing and validating forms. It's a performant library that improves the app performance by reducing the amount of code you need to write and minimizing the number of re-renders. RHF is also super light having zero dependencies. Lastly, you can integrate RHF with the majority of the available React UI libraries.

This article teaches you how to build and validate React forms using React Hook form. By the end of the article, you will have a working form with default input values and validation rules.

React Hook Form with validation and default values

The above video illustrates the form you will build.

Prerequisite

Make sure you install the following package in your project before continuing further:

react-hook-form

Build a form with React Hook Form

Let's start by building a form without any validation first. Import the useForm hook at the top of the file:

import { useForm } from 'react-hook-form';

useForm is a custom hook to manage the entire form. It allows you to set default form values, provide a validation schema, submit the form, plus other things.

Now add the following code after the import statement:

import { useForm } from 'react-hook-form';

export default function Form() {
    const {
        register,
        handleSubmit,
        formState: { errors },
    } = useForm();
}

In the above code, you initialize the useForm hook and then use destructuring to retrieve the required properties. The properties returned by the useForm hook include the following:

  • register: The method you use to register input and select elements for value tracking and validation.
  • handleSubmit: This method receives the form data if the form validation is successful. You use this method to handle the form submission.
  • formState: It's an object that holds information about the form state, including any validation errors, touched fields, dirty fields, and more.

Other properties are returned, but we're only interested in these three in this case. You can read more about this hook in the documentation.

Let's continue by rendering a form with the following input fields:

  • username
  • email
  • isAdmin
  • createdAt
import { useForm } from 'react-hook-form';

export default function Form() {
    const const {
        register,
        handleSubmit,
        formState: { errors },
    } = useForm();
    
    return (
        <form onSubmit={handleSubmit((d) => console.log(d))}>
            <div>
                <label htmlFor="username">Username</label>
                <input id="username" {...register('username')} />
            </div>

            <div>
                <label htmlFor="email">Email</label>
                <input id="email" {...register('email')} />
            </div>

            <div>
                <label htmlFor="isAdmin">IsAdmin</label>
                <input
                    id="isAdmin"
                    type="checkbox"
                    {...register('isAdmin')}
                />
            </div>

            <div>
                <label htmlFor="createdAt">Creation Date</label>
                <input
                    id="createdAt"
                    type="date"
                    {...register('createdAt')}
                />
            </div>

            <button type="submit">Submit</button>
        </form>
    );
}

Currently, the form outputs the data you submit to the console.

react-hook-form-without-validation

The form doesn't have any validation though, which is not ideal. So, in the next step, you'll add form validation using the mechanisms provided by React Hook Form.

Note: The above snippet doesn't include the styling to keep the code simple and focus on the essential bits. If you want to see the form with the TailwindCSS code, check out this gist.

Form validation with React Hook Form

Let's add the following validation logic:

  • the username is required, it should have at least 5 characters, and it must contain only letters, numbers and underscore
  • the email is required, it should be a maximum of 50 characters long, and it should be a valid email address
  • the createdAt date is required

Replace the first div for the username input with the following code:

<div>
  <label htmlFor="username">Username</label>
  <input
    id="username"
    {...register("username", {
      required: true,
      validate: {
        minLength: (v) => v.length >= 5,
        matchPattern: (v) => /^[a-zA-Z0-9_]+$/.test(v),
      },
    })}
  />

  {errors.username?.type === "required" && (
    <small>Username is required</small>
  )}

  {errors.username?.type === "minLength" && (
    <small>The username should have at least 5 characters</small>
  )}

  {errors.username?.type === "matchPattern" && (
    <small>Username must contain only letters, numbers and _</small>
  )}
</div>;

So, what's happening in the above code? The register function accepts two parameters - the name of the input/select element and a RegisterOptions object. The RegisterOptions object allows you to perform data validation, as shown in the above code snippet.

Let's take a closer look at the input field:

{...register("username", {
  required: true,
  validate: {
    minLength: (v) => v.length >= 5,
    matchPattern: (v) => /^[a-zA-Z0-9_]+$/.test(v),
  },
})}

The required flag is set to true, meaning users cannot submit the form without providing a username. The validate object allows you to define custom validation rules for the input field. In this example, it checks if the username is at least five characters long and if it matches the given regex expression.

{errors.username?.type === "required" && (
  <small>Username is required</small>
)}

{errors.username?.type === "minLength" && (
  <small>The username should have at least 5 characters</small>
)}

{errors.username?.type === "matchPattern" && (
  <small>Username must contain only letters, numbers and _</small>
)}

You can see the validation errors by accessing the errors object if any validation rules fail. For example, the above code displays error messages for the "username" input field based on the failed validation rule.

You can apply validation rules to the email field similarly. Replace the second div for the email input with the following code:

<div>
  <label htmlFor="email">Email</label>
  <input
    id="email"
    {...register("email", {
      required: "Email is required",
      validate: {
        maxLength: (v) =>
          v.length <= 50 || "The email should have at most 50 characters",
        matchPattern: (v) =>
          /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(v) ||
          "Email address must be a valid address",
      },
    })}
  />

  {errors.email?.message && (
    <small>{errors.email.message}</small>
  )}
</div>;

If you compare the code for the username input with the one for the email input, you should observe something different, though. The difference is that the email input code contains only one conditional rendering block for the errors.

Previously, there were three blocks for the username input field, one for each error type. The more validation rules you have, the more error code blocks you have. However, that can be improved by directly returning the error message when the validation rule fails:

validate: {
  maxLength: (v) =>
    v.length <= 50 || "The email should have at most 50 characters",
  matchPattern: (v) =>
    /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(v) || "Email address must be a valid address",

Instead of checking for each error type, you can return the validation error you provided in the validate object.

{errors.email?.message && (
  <small>{errors.email.message}</small>
)}

As a result, you have less code.

Up to this point, you learnt how to add multiple validation rules to a field. But what if you only want to add one validation rule, such as making the field required? You can do it as follows:

<div>
  <label htmlFor="createdAt">Creation Date</label>
  <input
    id="createdAt"
    type="date"
    {...register("createdAt", {
      required: "Date is required",
    })}
  />

  {errors.createdAt?.message && (
    <small>
      {errors.createdAt.message}
    </small>
  )}
</div>;

So, you learnt how to add one or more validation rules to the form input fields.

Set default values for the form

Before testing the form, let's define some default values for the form. You can do that by passing the defaultValues object with custom values for each input field to the useForm hook. In this case, the isAdmin flag is set to true by default. The rest of the fields are left empty.

const {
    register,
    handleSubmit,
    formState: { errors },
} = useForm({
    defaultValues: {
        username: "",
        email: "",
        isAdmin: true,
        createdAt: "",
    },
});

It's time to test the form and check if the default values and validation work correctly.

React Hook Form with validation

As the video illustrates, the form works as intended. The isAdmin flag is checked by default, and all the validation rules work correctly. You can see the complete form code in this gist.

Congrats! You have a working form with validation rules and default values.

If you're using React Hook Form, you might also be interested in the following articles: