How to create a form using React Hook Form library

In this article our team will guide you through the process of creating forms with React Hook Form library. We will describe installation and configuration as well as form building step by step. We will also provide examples of edge cases that our team encountered and resolved.

Authors: Nikola Laskowska, Karolina Kopacz, Rafał Stencel, Michał Dulko

Table of contents

What is the React Hook Form library?

React Hook Form is a library for creating performant, flexible and extensible forms. It enables adding validation in a simple way and integrates with UI libraries. It is worth mentioning that the library is small-sized and has no dependencies (source: npm). React Hook Form is an open-source solution which associates a big number of contributors all over the world. It results in the library being well-maintained and the releases being done several times a month.

The React Hook Form library worked perfectly for us during the DIAL Catalog of Digital Solutions project development. Many new features required of us to implement various types of forms. Thanks to this solution, we were able to build them efficiently. This one library was enough to write efficient, simple, and complex forms with validation. You can read the full case study on this project here.

Library installation and configuration


To start using the React Hook Form library you need to execute the following command in your terminal:

or

npm install react-hook-form

Next, import the useForm hook from the React Hook Form library:

import { useForm } from "react-hook-form"

Then, inside your component, use the hook as follows:


const { register, handleSubmit } = useForm()


It takes one object as optional argument, which props are: `mode, reValidateMode, defaultValues, values, shouldUnregister, shouldUseNativeValidation, resolver`.

UseFormProps – optional options object

mode

  • “onSubmit” (default) classic validation, when pressing the button to submit changes we have made.
  • “onBlur” validation will trigger (for the editing field) on the blur event, when leaving a field we are editing.
  • “onChange” validation will trigger on the change event with every input change, but it may lead to multiple re-renders. 

defaultValues

Works great mainly when editing a form. When we use the values from the backend of the props, for example, we want to edit a user, we can display in the form the values that currently exist in that user’s object and fill the fields with existing values. If you would like to use the same form for creating and editing, you can set the nullish coalescing operator (??) to assign values based on the database or to use default values for each field. Look at the example below:

defaultValues: {
  name: order.customerName ?? ‘default value’
}

Full list of optional options object can be found here.

UseFormReturn objects and functions returned from useForm hook

Functions

handleSubmit

The handleSubmit method manages form submission. It needs to be passed as the value to the onSubmit prop of the form component. We need to provide a callback to the handleSubmit function to manage the data we have provided in the next step in form. This function will receive the form data in the “data” object if form validation is successful.

setValue

Dynamically sets the value of a registered field.

getValues

An optimized helper for reading form values these values have to be registered.

watch

Used to track a field’s value which could be used, for example, for conditional rendering of content based on this field’s value. On the first load it takes values from setup defaultValues. In other cases it returns undefined for registered fields.

Objects

register

The register method helps you register an input field so that it is available for validation, and its value can be tracked for changes. Right now, anything you provide into this input will be registered and on submission, if validation is successful. The returned value will be passed in the ‘data’ object to the ‘handleSubmit’ function.

control

This object contains methods for registering components into React Hook Form. If we would like to use components other than uncontrolled components and native inputs, we have to use Controller and pass `control` as its property.

formState

Helps you to keep up with the user’s interaction with your form application. formState has many props, but we will focus only on the `errors` prop, which will help us track the form fields and return the errors on the field ‘changes’ or ‘submit a form’.

Full list of functions and objects can be found here

Building a validation-ready form

Once the backbone of the form’s logic has been configured, it is time to build the form’s structure. Let’s begin with creating an empty form.

Creating an empty form

Add the following `return` function to our component:

return (

  <form>

    <button type=”submit”>

      Submit

    </button>

  </form>

)

Then, create a function to be called on form submission (i.e. `placeOrder`) and pass it as an argument to the `handleSubmit` function:

const placeOrder = (data: Object) => { … }

return (

  <form onSubmit={handleSubmit(placeOrder)}>

    <button type=”submit”>

      Submit

    </button>

  </form>

)

Well done! You have successfully created the form’s scaffolding. In the next section of this article we will explain how to add user inputs to it.

Below you will find edge cases which our team encountered and resolved. Skip this section, if they do not apply to your project. 

Case: submit button is rendered in a different component

If the submit button is rendered in a component different from where the form is rendered, then assign an id of your choice (i.e. “my-form”) to `id` property of `form` element, and assign the same value to submit button’s `form` property, as follows:

Primary component:

return (

  <form onSubmit={handleSubmit(placeOrder)} id=”my-form”>

    <SecondComponent formId=”my-form” />

  </form>

)

Second component:

const SecondComponent = ({ formId }) => {

  return (

    <button type=”submit” form={formId}>

      Submit

    </button>

  )

}

Adding user inputs to the form

Once the form’s scaffolding has been created, we are ready to add user inputs. Let’s start with a simple text input for the user’s email let’s call this field “email.” The field needs to be registered so that its state is managed internally by the library. To register a field, call the `register` function with the field’s name as the only argument. Once registered, the field’s value will be present in the `data` object passed to the callback function called on form’s submission. Modify the form’s code into:

return (

  <form onSubmit={handleSubmit(placeOrder)}>

    <input {...register(“email”)} />

    <button type=”submit”>

      Submit

    </button>

  </form>

)

which is an equivalent of:

const { onChange, onBlur, name, ref } = register(“email”)

return (

  <form onSubmit={handleSubmit(placeOrder)}>

    <input 

      onChange={onChange}

      onBlur={onBlur}

      name={name}

      ref={ref}

    />

    <button type=”submit”>

      Submit

    </button>

  </form>

)

Below you will find edge cases which our team encountered and resolved. Skip this section, if they do not apply to your project. 

Case: use an external library controlled component as a user input

React Hook Form supports not only creating forms using uncontrolled components and native inputs, but also using internal and external controlled components (your custom design system inputs and the inputs from design systems such as React-Select, AntD, MUI) as well. To support using controlled components a wrapper component – `Controller` – has to be used. For example, to use `Checkbox` component from MUI to allow users to consent to some terms, you need to pass appropriate props to this component, as follows:

import { Controller } from “react-hook-form”

import Checkbox from “@mui/material/Checkbox”

(…)

return (

  <form onSubmit={handleSubmit(placeOrder)}>

    <Controller

      control={control}

      name="consent"

      render={({

        field: { onChange, onBlur, value, name, ref },

        fieldState: { invalid, isTouched, isDirty, error },

        formState

      }) => (

        <Checkbox

          onBlur={onBlur}

          onChange={onChange}

          checked={value}

          inputRef={ref}

        />

      )}

    />

    <button type=”submit”>

      Submit

    </button>

  </form>

)

Case: use your reusable component as a user input

In the form, you can use any reusable component which you have created. If the component’s properties match properties of a native input, then this component can be registered just like a native input:

Reusable input:

import React from “react”

export const ReusableComponent = React.forwardRef((props, ref) => (

  <input {...props} ref={ref} />

))

ReusableComponent.displayName = “ReusableComponent”

export default ReusableComponent

Primary component:

return (

  <form onSubmit={handleSubmit(placeOrder)}>

    <ReusableComponent {...register(“email”)} />

    <button type=”submit”>

      Submit

    </button>

  </form>

)

Adding validation to form inputs

We already know how to build a validate-ready form. Now it’s time for validation specific form fields. The React Hook Form gives us a lot of possibilities to validate the form correctly.

Validation properties

required

string | { value: boolean, message: string } 

Indicates that the input must have a value before the form can be submitted. You can simply assign a string, which means that this field is required and if it’s empty on submission, return the string to the errors object. Or you can provide an object with a value and a message which, if validation fails, will be returned in the errors object.

maxLength

{ value: number, message: string } 

The maximum length of the value to accept for this input. 

minLength

{ value: number, message: string }    

The minimum length of the value to accept for this input.    

max

{ value: number, message: string }    

The maximum value to accept for this input. 

min

{ value: number, message: string }    

The minimum value to accept for this input.    

pattern

{ value: RegExp, message: string }    

The regex pattern for the input.

validate

function | object

You can pass a callback function as the argument to validate, or you can pass an object of callback functions to validate all of them. This function will be executed on its own, without depending on other validation rules included in the required attribute.    

You can assign a string into a message to return an error message in the errors object, but it’s not necessary. If you choose not to do it, there won’t be any error messages.

Full list of validation properties can be found here.

The validation rules should be placed as an object in the ‘register’ function which was described here:

return (

  <form onSubmit={handleSubmit(placeOrder)}>

    <input

      {...register(“email”, {

        required: “This field is required.” 

      })} 

    />

    <button type=”submit”>

      Submit

    </button>

  </form>

)

How to define error messages

Once we’ve added this validation rule, the form will not be submitted if the field “name” is empty. If we want our form to be more user-friendly, it is recommended to add error messages.

The React Hook Form gives us possibilities to verify which field is invalid and to easily provide our custom error message. 

To do this we need to use “formState: { errors }” properties of the object returned from useForm hook. Then specify the error message and indicate where it should be displayed.

const { handleSubmit, register, formState: { errors } } = useForm()

return (

<form onSubmit={handleSubmit(placeOrder)}>

  <input

    {...register(“email”, {

      required: “This field is required”

    })}

  />

  {errors.email && <p>{errors.email?.message}</p>}

  <button type=”submit”>

    Submit

  </button>

</form>

)

To verify whether our field fulfill some requirements and then if it does not, we want to display an error message, then we can give an error message after “||” syntax (Check: “Case: Validate email address” section).

If we want to add different error messages for a field which depends on validated results, we can add them to the function body (Check: “Case: Async email validation using endpoint and setting multiple error messages” section).

Case: Validate email address

To validate email addresses in a form, we recommend using external packages, for example email-validator.

import { validate } from “email-validator”

(…)

const { handleSubmit, register, formState: { errors } } = useForm()

return (

  <form onSubmit={handleSubmit(placeOrder)}>

    <input

      {...register(“email”, {

        required: “This field is required”,

        validate: value => validate(value) || “Please enter a valid email address”

      })}

    />

    {errors.email && <p>{errors.email?.message}</p>}

    <button type=”submit”>

      Submit

    </button>

  </form>

)

Case: Async email validation using endpoint and setting multiple error messages 

We know how to simply validate an email address, but what if we need to also check if the provided email already exists in the database? We need to add more requirements. The simplest way to achieve this is to extract two requirements into one function.

import { validate } from “email-validator”

(…)

const { handleSubmit, register, formState: { errors } } = useForm()

const isUniqueUserEmail = async (value) => {

  // first we check if provided email is valid

  const isEmailValid = validate(value)

  // then we call our isEmailUsed function to check if email already exists in the database, return true if exists, false if not

  return await isEmailUsed({ email: value }).then((userEmailCheck) => {

    if (isEmailValid & userEmailCheck) {

      return “This email is already assigned”

    } else if (!isEmailValid) {

      return “Please enter a valid email address”

    }

    return true

  })

}

return (

  <form onSubmit={handleSubmit(placeOrder)}>

    <input

      {...register(“email”, {

        required: “This field is required”,

        validate: value => validate(value)

     })} 

    />

    {errors.email && <p>{errors.email?.message}</p>}

    <button type=”submit”>

      Submit

    </button>

  </form>

)

Case: Validate one field depending on another one

Sometimes validation of one field depends on another field in the same form. In this example, the user needs to provide an email address and select their organization, but the organization domain must be the same as the provided email domain. In our project, we wanted to add validation on both the backend and the frontend. The function responsible for those validations should be called each time an email or a select value changes.

In this case, what comes in useful  is a function already built-in in the react-hook-form getValues.

import { validate } from “email-validator”

(...)

const { handleSubmit, register, getValues, formState: { errors } } = useForm()

const validateOrganizationDomain = (value) => {

  // we use .split('@')[1] because we need only email domain

  if (value && !value.domain.includes(getValues('email').split('@')[1])) {

    return “The email address must match the organization’s domain”

  }

  return true

}

return (

  <form onSubmit={handleSubmit(placeOrder)}>

    <input 

      {...register(“email”, {

        required: “This field is required”,

        validate: value => validate(value) 

      })} 

    />

    {errors.email && <p>{errors.email?.message}</p>}

    <select

      {...register(“organization”, {

        required: “This field is required”,

        validate: validateOrganizationDomain 

       })} 

      placeholder=”Select organization”

    >

      <option value="organization_1">Organization 1</option>

      <option value="organization_2">Organization 2</option>

    </select>

    {errors.organization && <p>{errors.organization?.message}</p>}

    <button type=”submit”>

      Submit

     </button>

  </form>

)

Case: Validate using Controller

A validation field which is wrapped in the Controller is a little bit different. Here we need to use prop rules where we can add all the rules described above.

Example:

import { Controller } from “react-hook-form”

import Checkbox from “@mui/material/Checkbox”

(...)

const { handleSubmit, register, getValues, formState: { errors } } = useForm()

return (

  <form onSubmit={handleSubmit(placeOrder)}>

    <Controller

      control={control}

      name="consent"

      render={({

        field: { onChange, onBlur, value, name, ref },

        fieldState: { invalid, isTouched, isDirty, error },

        formState

      }) => (

        <Checkbox

          onBlur={onBlur}

          onChange={onChange}

          checked={value}

          inputRef={ref}

        />

      )}

      rules={{ required: “This field is required” }}

    />

    {errors.consent && <p>{errors.consent?.message}</p>}

    <button type="submit">

    Submit

    </button>

  </form>

)

Conclusion

Advantages

  • Supports all features we could think of.
  • A lot of customization options will fit most, if not all use cases.
  • Extensible ability to create custom validation functions.
  • Suitable for both simple and complex forms.
  • Suitable for both JavaScript and TypeScript projects.
  • Big community, well-maintained.

Disadvantages

  • Steep learning curve due to a lot of functionality this library supports.
  • Documentation is chaotic and poorly organized, and misses some details, so it is often hard to find what you are looking for.
  • Does not offer any visual components to complement logic it offers – developers need to provide a way of displaying validation error messages on their own and style inputs appropriately.

A few words on React Hook Form library from our team:

Would I choose the library again? Always! I have not seen a better library to manage and track form values in every form we created.

Michał Dulko
Frontend Developer

Once we understand how the library works, I can safely say that this library will become our best friend in creating forms. Would I choose the React Hook Form library again? – I already did it in another project 🙂

Karolina Kopacz
Frontend Developer

React Hook Form met all our expectations. It is a complete solution for handling complex form logic and I believe it saved us a lot of time and effort. Once mastered, it is very easy to use.

Rafał Stencel
Frontend Developer