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:
yarn add react-hook-form
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.
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 🙂
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.