Building forms with React Hook Form and Yup
In almost every web application, there are forms where the user enters data, whether it is a login or registration form, a passport application form, a bank account creation form, or just a simple contact us form, forms are an essential part of how users interact with a website.
As frontend developers, our main goal while building these forms is to collect the data from the end-user in order to perform some actions afterward, send the data to an API, make some secure pages available after successful authentication, or otherwise show some error messages, etc. And hopefully doing all this in the cleanest and performant way possible.
Now before jumping into React Hook Form and Yup, the first question that pops into your head is how do we usually do this in React with no third-party libraries involved?
For that let's take a look at the code below:
import React, { useState } from 'react';
const MyFormComponent: React.FC = () => {
const [state, setState] = useState({
username: '',
email: '',
password: '',
});
const { username, email, password } = state;
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = event.target;
setState(prevState => ({
...prevState,
[name]: value,
}));
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
console.log(state);
};
return (
<form onSubmit={handleSubmit}>
<div>
<label>Username</label>
<input type="text" name="username" value={username} onChange={handleChange} />
</div>
<div>
<label>Email</label>
<input type="text" name="email" value={email} onChange={handleChange} />
</div>
<div>
<label>Password</label>
<input type="password" name="password" value={password} onChange={handleChange} />
</div>
<div>
<button type="submit">Register</button>
</div>
</form>
);
};
export default MyFormComponent;
In the above example, we use React one-way data binding to create a simple registration form with only 3 input fields: username, email and password, and a submit button, each input has a value and onChange handler to update that value in our state, and we also have a handleSubmit function that just logs the form data.
Now there are two main issues with that approach:
The performance:
Let's assume you have 5 to 10 inputs in your form which is the usual range, now every time the user types or deletes a character, the onChange event will be triggered and the handleChange function will be executed to update our state. Hence the DOM will re-render which will have a considerable impact on our application performance.
The code quality:
At this point, the code example above looks fine. But what if you have too many inputs with multiple validation rules to have on those inputs and display the errors to the user, the code will become more complex and lengthy.
Adding to that some bad practices such as long unreadable functions, hard-coding, tight coupling, etc. Things might get a little messy 👀 and the code refactoring would become almost an impossible task.
In this article, we will see what react-hook-form has to offer in order to help us avoid these issues when it comes to handling forms, and we will mainly focus on how to integrate and use react-hook-form in our react application. We'll also see how it's used with UI libraries like Material-UI, and we will wrap it up with Yup validation.
Why React Hook Form?
React Hook Form is a React library that is used to make performant, flexible, and extensible forms with easy-to-use validation support.
Sure, there have been other libraries like Formik that fall under similar lines but here’s why it goes above all those:
- Small in size: for those of you who are bundlephobic 😱, the package size is very tiny and minimal: just 8.4KB minified + gzipped, and it has zero dependencies
- Performance: React Hook Form uses ref instead of depending on the state to control the inputs, which makes the forms more performant and reduces the number of re-renders
- Clean code and readability: almost every form handling scenario is covered all in fewer lines of code
- Uses existing HTML: you can leverage the powers of HTML to work with the library
- UI libraries support: painless integration with UI libraries since most of them support ref
- Validation resolver: gives you what's called a resolver that allows you to validate your form using a custom validation library like Yup, Zod, etc.
What are the basics I must know?
To install React Hook Form, run the following command from the root folder of your react application:
yarn add react-hook-form
The react-hook-form library provides a useForm
hook which you can import like this:
import { useForm } from 'react-hook-form';
Then inside your component, you can use the hook:
const {
register,
handleSubmit,
setValue,
reset,
watch,
getValues,
formState: { errors },
} = useForm({
defaultValues: initialValues,
mode: 'onChange',
resolver: yupResolver(schema),
});
The useForm
hook takes and returns two objects containing few properties, here are the most commonly-used ones:
Input
defaultValues
The defaultValues are used as the initial value when a component is first rendered before a user interacts with itmode
It can be passed to useForm hook if you want to validate the field whenever there is an onChange event for example. the possible values are onChange, onBlur, onSubmit, onTouched, all. With onSubmit being the default valueresolver
This function allows you to use any external validation library such as Yup, Zod, Joi, Superstruct, Vest, and many others, we will look at it in more detail when we get to the validation section
Output
register
is a function to be assigned to each input field ref so that React Hook Form can track the changes for the input field valuehandleSubmit
is the function that is called when the form is submittedsetValue
is a function that allows you to dynamically set the value of a registered fieldreset
as its name suggests is a function that resets the values and errors of the fieldwatch
allows you to observe form changes, it will notify you whenever an input changes. This way you don’t have to wait for the user to submit the form in order to do somethinggetValues
is an optimized helper for reading form values. The difference between watch and getValues is that getValues will not trigger re-renders or subscribe to input changeserrors
is an object that will contain the validation errors of each input if any
How to create my first form using React Hook Form and Material-UI?
Now that you have an idea about the basic usage of the useForm
hook, let's rewrite the code for our first form example using this time react-hook-form:
import React from 'react';
import { useForm } from 'react-hook-form';
import { TextField, Button } from '@material-ui/core';
interface MyForm {
username: string;
email: string;
password: string;
}
const MyReactHookFormComponent: React.FC = () => {
const { register, handleSubmit } = useForm<MyForm>({
mode: 'onChange',
});
const submitForm = (data: MyForm) => {
console.log({ data });
};
return (
<form onSubmit={handleSubmit(submitForm)}>
<TextField inputRef={register} name="username" label="Username" />
<TextField inputRef={register} name="email" label="Email" />
<TextField inputRef={register} name="password" label="Password" type="password" />
<Button type="submit">Register</Button>
</form>
);
};
export default MyReactHookFormComponent;
As you can see, the useForm
hook makes our component code cleaner and maintainable, which makes adding either new fields or validation very easy and straightforward.
In this example, we use Material-UI as a design library. You may notice that we pass the register method to Textfield's inputRef prop, this is because the Material-UI uncontrolled form components give access to the native DOM input using the inputRef
prop.
If we were to use a simple HTML input tag we would write:
<input ref={register} name="email" />
Note that in addition to the inputRef
, we have given each TextField a unique name which is mandatory so react-hook-form can track the changing data.
Also, we added the onSubmit function which is passed to the handleSubmit. When we submit the form, the handleSubmit function will handle the form submission. It will send the entered data to the onSubmit function which we’re logging to the console.
How to use Controlled inputs in React Hook Form?
For some UI libraries, there are components that don't support a ref input and can only be controlled. For that use case, react-hook-form has a wrapper component called Controller that will make it easier for you to manipulate them:
Controller
: allows you to register a third party controlled component using the control object
import { Controller } from 'react-hook-form';
control
: replaces the register method when it comes to controlled components
const { control } = useForm();
Now let's say we want to add a country field to our form
We first add the country field to MyForm interface:
interface MyForm {
// ...
country: string;
}
and then we add the Controller component:
<Controller
control={control}
name={'country'}
as={TextField}
label="Country"
select
>
{countries &&
countries.map(country => (
<MenuItem key={country.code} value={country.code}>
{country.label}
</MenuItem>
))}
</Controller>
Note that in addition to the control prop, the Controller component accepts the input name, label, and type which is a select in our example.
We pass our controlled component to the Controller using the as
prop.
How to add error validation to my form?
There are two ways you can use to add validation to your form.
You can leverage react-hook-form's built-in validation by passing your rules to the register method, here is a simple example of how you can do it:
<TextField
inputRef={register({
required: 'Password is required',
minLength: {
value: 8,
message: 'Password must have at least 8 characters',
},
})}
name="password"
label="Password"
type="password"
/>
As you can see we've passed an object containing two validation rules, required and minLength, to register.
We must all agree that mixing validation rules with HTML code is clearly not a good practice, especially when it comes to a more real-world example where we would have multiple inputs, each one of them having plenty of rules.
Ideally, the best solution here would be to separate our HTML and the validation code.
For that, React Hook Form supports external schema-based form validation with Yup, where you can pass your schema to useForm. React Hook Form will validate your input data against the schema and return with either errors or a valid result.
In order to implement validation using Yup, start by adding yup and @hookform/resolvers to your project:
yarn add yup @hookform/resolvers
Then import Yup, and create your schema. It is considered a best practice to define your schema in a separate file:
import * as yup from 'yup';
export const schema = yup.object().shape({
username: yup.string().required('Username is required please !'),
email: yup
.string()
.email('Please enter a valid email format !')
.required('Email is required please !'),
password: yup
.string()
.min(4, 'Password must contain at least 4 characters')
.required('Password is required please !'),
country: yup.string().required('Country is required please !')
});
Here we create a schema for our input fields:
- All fields are required
- The email format must be valid
- The password must contain at least 4 characters
Note that if you don't specify an error message to your rule, the default message will be displayed.
📄 You can find plenty of other validation rules for various use cases in the yup documentation.
Use your resolver, yupResolver
in our case, in order to add your schema to the form's input values
// ...
import { schema } from './schema';
import { yupResolver } from '@hookform/resolvers/yup';
const {
register,
handleSubmit,
formState: { errors },
} = useForm<MyForm>({
resolver: yupResolver(schema),
});
And then you can access the errors object from the useForm hook to display error messages:
<TextField
// ...
error={!!errors.username}
helperText={errors.username && errors?.username?.message}
/>
If your errors
object contains the username field, the error message will be displayed.
Bonus: yup and react-intl
Many applications use internationalization (i18n) libraries to handle translation into different languages. One of the most used libraries is react-intl.
In case you're using yup with react-intl, in your en.json
file, add the error message id and its corresponding value:
{
// ...
"error-message-id": "Username is required please !",
}
Add the same id to the other files: ar.json, fr.json, etc.
Then, pass the error message id to the required function
username: yup.string().required('error-message-id'),
And pass the id to intl formatMessage
function to display the error message
<TextField
// ...
error={!!errors.username}
helperText={
errors.username &&
intl.formatMessage({
id: errors?.username?.message,
})
}
/>
Now your error messages will be translated depending on the user's local.
Conclusion
React Hook Form is a very performant and straightforward library to build and validate forms in React. Requiring fewer lines of code, with minimal package size, not to forget the easy integration with Ui libraries and external validation resolvers. React Hook Form guarantees not only a great user experience but also a better developer one.