Creating forms in React is a complex task. It involves handling all the input states and their changes and validating that input when the form gets submitted.
For simple forms, things are generally manageable. But as your form gets more complex and you need to add various validations, it becomes a complicated task.
So instead of manually writing all of the code and handling complex forms with validation logic, we can use the most popular React library for this, react-hook-form.
It's the most popular React library for creating forms compared to formik, react final form, and others, and I use it for all my client projects.
In this article, we will explore how to use the react-hook-form library in detail.
So let's get started.
Why the react-hook-form Library is the Most Popular Form Library in React
Following are some of the reasons why react-hook-form is a popular choice for creating React forms.
- The number of re-renders in the application is smaller compared to the alternatives because it uses refs instead of state.
- The amount of code you have to write is less as compared to formik, react-final-form and other alternatives.
- react-hook-form integrates well with the yup library for schema validation so you can combine your own validation schemas.
- Mounting time is shorter compared to other alternatives.
Check out the react-hook-form website for more detailed comparison.
How to Create a Form Without Using a Library
Before creating a form using the react-hook-form library, let's create a simple form without using any library.
Take a look at the below code:
import React, { useState } from "react";
import "./styles.css";
export default function App() {
const [state, setState] = useState({
email: "",
password: ""
});
const handleInputChange = (event) => {
const { name, value } = event.target;
setState((prevProps) => ({
...prevProps,
[name]: value
}));
};
const handleSubmit = (event) => {
event.preventDefault();
console.log(state);
};
return (
<div className="App">
<form onSubmit={handleSubmit}>
<div className="form-control">
<label>Email</label>
<input
type="text"
name="email"
value={state.email}
onChange={handleInputChange}
/>
</div>
<div className="form-control">
<label>Password</label>
<input
type="password"
name="password"
value={state.password}
onChange={handleInputChange}
/>
</div>
<div className="form-control">
<label></label>
<button type="submit">Login</button>
</div>
</form>
</div>
);
}
Here's a Code Sandbox demo.
In the above code, we have only two input fields, namely email
and password
and a submit button.
Each input field has a value
and onChange
handler added so we can update the state based on the user's input.
Also, we have added a handleSubmit
method which displays the data entered in the form to the console.
This looks fine. But what if we need to add validations like required field validation, minimum length validation, password validation, email field validation and also display the corresponding error messages?
The code will become more complex and lengthy as the number of input fields and their validations increases.
How to Install react-hook-form Library
Displaying forms is a very common requirement in any application.
So let’s learn why and how to use react-hook-form
. For that, we'll create a new React application.
Create a new React project by running the following command from the terminal:
create-react-app demo-react-hook-form
Once the project is created, delete all files from the src
folder and create new index.js
and styles.css
files inside the src
folder.
To install the react-hook-form
library, execute the following command from the terminal:
npm install react-hook-form@7.38.0
OR
yarn add react-hook-form@7.38.0
Here, we're installing version 7.38.0
of the react-hook-form
library which is the latest version at the time of writing this article.
How to Create Initial Pages
Open the src/index.js
file and add the following content inside it:
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
Note that the above code is using React version 18+ syntax for rendering the app.
If you're using React version less than 18 (which you can confirm from the package.json
file), then add the following code in your src/index.js
file.
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
Now, open the src/styles.css
file and add the content from here inside it.
Now, create a new file called App.js
inside the src
folder with the following content:
import React from "react";
import "./styles.css";
export default function App() {
return (
<div className="App">
<form>
<div className="form-control">
<label>Email</label>
<input type="text" name="email" />
</div>
<div className="form-control">
<label>Password</label>
<input type="password" name="password" />
</div>
<div className="form-control">
<label></label>
<button type="submit">Login</button>
</div>
</form>
</div>
);
}
Here, we have just added the email and password fields to the form.
How to Create a Basic Form with react-hook-form
The react-hook-form
library provides a useForm
hook which we can use to work with forms.
Import the useForm
hook like this:
import { useForm } from 'react-hook-form';
You can use the useForm
hook like this:
const {
register,
handleSubmit,
formState: { errors },
} = useForm();
Here,
-
register
is a function provided by theuseForm
hook. We can assign it to each input field so that thereact-hook-form
can track the changes for the input field value -
handleSubmit
is the function we can call when the form is submitted -
errors
is a nested property in theformState
object which will contain the validation errors, if any
Now, replace the contents of the App.js
file with the following code:
import React from "react";
import { useForm } from "react-hook-form";
import "./styles.css";
export default function App() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<div className="App">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="form-control">
<label>Email</label>
<input type="text" name="email" {...register("email")} />
</div>
<div className="form-control">
<label>Password</label>
<input type="password" name="password" {...register("password")} />
</div>
<div className="form-control">
<label></label>
<button type="submit">Login</button>
</div>
</form>
</div>
);
}
In the above code, we have added a register function to each input field that we got from the useForm
hook by passing a unique name to each register
function like this:
{...register("email")}
We're using the spread operator so react-hook-form
will spread out all the required event handlers like onChange
, onBlur
, and other props for that input field.
If you add a console.log({ ...register("email") });
inside the component, you will see what it returns as can be seen below:
We also added the onSubmit
function which is passed to the handleSubmit
method like this:
<form onSubmit={handleSubmit(onSubmit)}>
...
Note that, you need to pass a unique name to the register
function added for each input field so react-hook-form
can track the changing data.
When we submit the form, the handleSubmit
function will handle the form submission. It will send the user entered data to the onSubmit
function where we're logging the user data to the console.
const onSubmit = (data) => {
console.log(data);
};
Now, start the application by running the npm start
or yarn start
command and you will see the following screen:
Here's a Code Sandbox demo.
As you can see, when we submit the form, the details entered by the user are displayed in the console.
Also, as compared to the code without react-hook-form
(which we saw at the start of this article in this Code Sandbox demo), this code is much simpler.
This is because we don’t have to add the value
and onChange
handler for each input field and there is no need to manage the application state ourselves.
How to Add Validations to the Form
Now, let’s add the required field and minimum length validation to the input fields.
To add validation we can pass an object to the register
function as a second parameter like this:
<input
type="text"
name="email"
{...register("email", {
required: true
})}
/>
<input
type="password"
name="password"
{...register("password", {
required: true,
minLength: 6
})}
/>
Here, for the email field, we're specifying required field validation. For the password field we're specifying the required field and minimum 6 character length validation.
When the validation fails, the errors
object coming from the useForm
hook will be populated with the fields for which the validation failed.
So we will use that errors
object to display custom error messages.
Open the App.js
file and replace its contents with the following content:
import React from "react";
import { useForm } from "react-hook-form";
import "./styles.css";
export default function App() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<div className="App">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="form-control">
<label>Email</label>
<input
type="text"
name="email"
{...register("email", {
required: true,
pattern: /^[^@ ]+@[^@ ]+\.[^@ .]{2,}$/
})}
/>
{errors.email && errors.email.type === "required" && (
<p className="errorMsg">Email is required.</p>
)}
{errors.email && errors.email.type === "pattern" && (
<p className="errorMsg">Email is not valid.</p>
)}
</div>
<div className="form-control">
<label>Password</label>
<input
type="password"
name="password"
{...register("password", {
required: true,
minLength: 6
})}
/>
{errors.password && errors.password.type === "required" && (
<p className="errorMsg">Password is required.</p>
)}
{errors.password && errors.password.type === "minLength" && (
<p className="errorMsg">
Password should be at-least 6 characters.
</p>
)}
</div>
<div className="form-control">
<label></label>
<button type="submit">Login</button>
</div>
</form>
</div>
);
}
If you check the application now, you will see the following screen:
Here's a Code Sandbox demo.
As you can see, we're getting instant validation errors for each input field once we submit the form and then try to enter the values in the input fields.
If there is any error for any of the input field, the errors
object will be populated with the type of error which we're using to display our own custom error message like this:
{errors.email && errors.email.type === "required" && (
<p className="errorMsg">Email is required.</p>
)}
{errors.email && errors.email.type === "pattern" && (
<p className="errorMsg">Email is not valid.</p>
)}
Here, based on the type of error, we're displaying different error messages.
Using the ES11 optional chaining operator, you can further simplify the above code like this:
{errors.email?.type === "required" && (
<p className="errorMsg">Email is required.</p>
)}
{errors.email?.type === "pattern" && (
<p className="errorMsg">Email is not valid.</p>
)}
In the similar way, we have added the password field validation.
Also, as you can see, each input field is automatically focused when we submit the form if there is any validation error for that input field.
Also, the form is not submitted as long as there is a validation error. If you check the browser console, you will see that the console.log
statement is only printed if the form is valid and there are no errors.
So using react-hook-form
reduced the amount of code that we had to write. The validation is also responsive, so once the field becomes valid, the error message goes away instantly.
But as the number of validations for each field increases, the conditional checks and error message code will still increase. So we can further refactor the code to make it even simpler.
Take a look at the below code:
import React from "react";
import { useForm } from "react-hook-form";
import "./styles.css";
export default function App() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<div className="App">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="form-control">
<label>Email</label>
<input
type="text"
name="email"
{...register("email", {
required: "Email is required.",
pattern: {
value: /^[^@ ]+@[^@ ]+\.[^@ .]{2,}$/,
message: "Email is not valid."
}
})}
/>
{errors.email && <p className="errorMsg">{errors.email.message}</p>}
</div>
<div className="form-control">
<label>Password</label>
<input
type="password"
name="password"
{...register("password", {
required: "Password is required.",
minLength: {
value: 6,
message: "Password should be at-least 6 characters."
}
})}
/>
{errors.password && (
<p className="errorMsg">{errors.password.message}</p>
)}
</div>
<div className="form-control">
<label></label>
<button type="submit">Login</button>
</div>
</form>
</div>
);
}
In the code above, we have changed the email and password validation code.
For the email input field, we changed this previous code:
{...register("email", {
required: true,
pattern: /^[^@ ]+@[^@ ]+\.[^@ .]{2,}$/
})}
to the below code:
{...register("email", {
required: "Email is required.",
pattern: {
value: /^[^@ ]+@[^@ ]+\.[^@ .]{2,}$/,
message: "Email is not valid."
}
})}
Here, we’ve directly provided the error message we want to display while adding the validation itself.
So we no longer need to add extra checks for each validation. We are displaying the error message using the message
property available inside the errors
object for each input field like this:
{errors.email && <p className="errorMsg">{errors.email.message}</p>}
So by doing it this way, the code is further simplified which makes it easy to add extra validations in the future.
Note that, if there are validation errors, the onSubmit handler will not be executed and the corresponding input field will automatically be focused (which is a good thing).
Here's an updated Code Sandbox demo.
How to Add Multiple Validations
You can even provide multiple validations for the input field by adding a validate
object. This is useful if you need to perform complex validations like this:
<input
type="password"
name="password"
{...register("password", {
required: true,
validate: {
checkLength: (value) => value.length >= 6,
matchPattern: (value) =>
/(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?!.*\s)(?=.*[!@#$*])/.test(
value
)
}
})}
/>
and to display the error messages, we use it like this:
{errors.password?.type === "required" && (
<p className="errorMsg">Password is required.</p>
)}
{errors.password?.type === "checkLength" && (
<p className="errorMsg">
Password should be at-least 6 characters.
</p>
)}
{errors.password?.type === "matchPattern" && (
<p className="errorMsg">
Password should contain at least one uppercase letter, lowercase
letter, digit, and special symbol.
</p>
)}
Here's a Code Sandbox demo.
How to Reset the Form Values
Sometimes, we need to reset/clear the data entered by the user after some action.
For example, once the form is submitted, we want to show the success message and then clear the form data so the user should not re-submit the same data.
In such a case, we can call the reset
function returned by the useForm
hook to clear the form data.
const { reset } = useForm();
Here's a Code Sandbox demo.
The reset
function also accepts an optional object where you can pass the values you want the form data to reset:
reset({
username: "Alex",
email: "alex@example.com",
password: "Test@123"
});
Here, the key username
, email
or password
should match with the name passed to the register
function so the respective input field will be set to the passed value.
How to Set Initial Form Values Using defaultValues
The useForm
hook accepts a list of options, one of which is defaultValues
.
Using defaultValues
we can set initial values for the form elements and re-set them when moving from one page to another like this:
const { user } = props;
const {
register,
handleSubmit,
formState: { errors }
} = useForm({
defaultValues: {
first_name: user.first_name,
last_name: user.last_name
}
});
// JSX
<Form.Control
type="text"
{...register("first_name")}
/>
<Form.Control
type="text"
{...register("last_name")}
/>
In the above code, for the register
function we've passed first_name
as the name. This means that in the defaultValues
we're using the same name to set the initial value.
So to correctly set the input value, you need to use the same name used in the register
function for setting the initial value using defaultValues
.
Here's a Code Sandbox demo.
How to Use react-hook-form with Other Libraries
Sometimes, we might be using some external libraries like react-select to allow multiple selection in a dropdown.
In such cases, we can't directly add the register
function for showing the select dropdown. So If we want to add react-hook-form
validations without writing our own code and handler function, we can use the Controller
component from react-hook-form
like this:
import React from "react";
import { useForm, Controller } from "react-hook-form";
import Select from "react-select";
import "./styles.css";
const departments = [
{ value: "Computer-Science", label: "Computer Science" },
{ value: "Physics", label: "Physics" },
{ value: "Chemistry", label: "Chemistry" },
{ value: "Mathematics", label: "Mathematics" }
];
export default function App() {
const {
control,
handleSubmit,
formState: { errors }
} = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<div className="App">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="form-control">
<label>Select Department of Interest</label>
<Controller
name="department"
control={control}
rules={{ required: true }}
render={({ field }) => (
<Select {...field} isMulti options={departments} />
)}
/>
{errors.department && (
<p className="errorMsg">This is a required field.</p>
)}
</div>
<div className="form-control">
<label></label>
<button type="submit">Submit</button>
</div>
</form>
</div>
);
}
Here's a Code Sandbox demo.
As you can see in the above code, we're importing the Controller
component at the top:
import { useForm, Controller } from "react-hook-form";
and control
from useForm
hook like this:
const {
control,
handleSubmit,
formState: { errors }
} = useForm();
Note, we're not using the register
function here.
Regular use of the react-select
library to allow multiple selection goes like this:
import Select from "react-select";
// use
<Select isMulti options={options} />
But to use it with react-hook-form
we need to wrap it in the Controller
component:
<Controller
name="department"
control={control}
rules={{ required: true }}
render={({ field }) => (
<Select {...field} isMulti options={options} />
)}
/>
Here, we have to give a unique value for the name
prop in the Controller
.
The validations are added as a part of the rules
prop and we use the render
prop to render the Select
dropdown.
How to Use Other Input Types with react-hook-form
In this section, we'll see how to use radio buttons and checkboxes with react-hook-form
.
Take a look at the below code:
import React from "react";
import { Button, Form } from "react-bootstrap";
import { useForm } from "react-hook-form";
import "./styles.css";
export default function App() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm();
const onSubmit = (data) => {
console.log(data);
};
return (
<div className="container">
<form onSubmit={handleSubmit(onSubmit)}>
<Form.Group className="mb-3" controlId="email">
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
placeholder="Enter your email"
{...register("email", {
required: "Please enter your email",
pattern: {
value: /^[^@ ]+@[^@ ]+\.[^@ .]{2,}$/,
message: "Please enter a valid email"
}
})}
/>
{errors.email && <p className="errorMsg">{errors.email.message}</p>}
</Form.Group>
<Form.Group className="mb-3" controlId="gender">
<Form.Label>Select Gender</Form.Label>
<Form.Check
type="radio"
label="Male"
value="male"
{...register("gender", {
required: "Please select your gender"
})}
/>
<Form.Check
type="radio"
label="Female"
value="female"
{...register("gender")}
/>
{errors.gender && <p className="errorMsg">{errors.gender.message}</p>}
</Form.Group>
<Form.Group className="mb-3" controlId="skills">
<Form.Label>Select Your Skills</Form.Label>
<Form.Check
type="checkbox"
label="JavaScript"
value="JavaScript"
{...register("skills", {
required: "Please select at-least one skill"
})}
/>
<Form.Check
type="checkbox"
label="React"
value="react"
{...register("skills")}
/>
<Form.Check
type="checkbox"
label="Node.js"
value="nodejs"
{...register("skills")}
/>
<Form.Check
type="checkbox"
label="Angular"
value="angular"
{...register("skills")}
/>
{errors.skills && <p className="errorMsg">{errors.skills.message}</p>}
</Form.Group>
<label></label>
<Button type="submit" variant="primary">
Submit
</Button>
</form>
</div>
);
}
Here's a Code Sandbox demo.
In the above code, I'm using react-bootstrap to make the UI look good, so Form.Check
is a react-bootstrap
component.
The main point you need to remember is that we've not given the same names for the register
function for a group of radio buttons and checkboxes like this:
<Form.Check
type="radio"
label="Male"
value="male"
{...register("gender", {
required: "Please select your gender"
})}
/>
<Form.Check
type="radio"
label="Female"
value="female"
{...register("gender")}
/>
In the above code, we've given gender
as the name for both the radio buttons and skills
as the name for all the checkboxes as shown below:
<Form.Check
type="checkbox"
label="JavaScript"
value="JavaScript"
{...register("skills", {
required: "Please select at-least one skill"
})}
/>
<Form.Check
type="checkbox"
label="React"
value="react"
{...register("skills")}
/>
<Form.Check
type="checkbox"
label="Node.js"
value="nodejs"
{...register("skills")}
/>
<Form.Check
type="checkbox"
label="Angular"
value="angular"
{...register("skills")}
/>
Also, note that the required field validation is added only for the first radio button or checkbox. Because we're using the same name, we don't need to add the same validation to each radio button or checkbox.
How to Set Initial Values for Radio Buttons And Checkboxes
Sometimes we may have pre-selected radio buttons or checkboxes which we need to display on page load, in such a case, we can again use the defaultValues
option from for the useForm
hook.
Take a look at the below code:
import React from "react";
import { Button, Form } from "react-bootstrap";
import { useForm } from "react-hook-form";
import "./styles.css";
const initialValues = {
gender: "male",
skills: {
JavaScript: true,
react: false,
nodejs: true,
angular: false
}
};
export default function App() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm({
defaultValues: {
gender: initialValues.gender,
skills: Object.keys(initialValues.skills).filter(
(item) => initialValues.skills[item] === true
)
}
});
const onSubmit = (data) => {
console.log(data);
};
return (
<div className="container">
<form onSubmit={handleSubmit(onSubmit)}>
<Form.Group className="mb-3" controlId="email">
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
placeholder="Enter your email"
{...register("email", {
required: "Please enter your email",
pattern: {
value: /^[^@ ]+@[^@ ]+\.[^@ .]{2,}$/,
message: "Please enter a valid email"
}
})}
/>
{errors.email && <p className="errorMsg">{errors.email.message}</p>}
</Form.Group>
<Form.Group className="mb-3" controlId="gender">
<Form.Label>Select Gender</Form.Label>
<Form.Check
type="radio"
label="Male"
value="male"
{...register("gender", {
required: "Please select your gender"
})}
/>
<Form.Check
type="radio"
label="Female"
value="female"
{...register("gender")}
/>
{errors.gender && <p className="errorMsg">{errors.gender.message}</p>}
</Form.Group>
<Form.Group className="mb-3" controlId="skills">
<Form.Label>Select Your Skills</Form.Label>
<Form.Check
type="checkbox"
label="JavaScript"
value="JavaScript"
{...register("skills", {
required: "Please select at-least one skill"
})}
/>
<Form.Check
type="checkbox"
label="React"
value="react"
{...register("skills")}
/>
<Form.Check
type="checkbox"
label="Node.js"
value="nodejs"
{...register("skills")}
/>
<Form.Check
type="checkbox"
label="Angular"
value="angular"
{...register("skills")}
/>
{errors.skills && <p className="errorMsg">{errors.skills.message}</p>}
</Form.Group>
<label></label>
<Button type="submit" variant="primary">
Submit
</Button>
</form>
</div>
);
}
Here's a Code Sandbox demo.
In the above code, we have an initialValues
object which contains the values we want to set on the initial page load:
const initialValues = {
gender: "male",
skills: {
JavaScript: true,
react: false,
nodejs: true,
angular: false
}
};
As we can have multiple skills, skills
is an object as shown above. So we want to show the radio button selected if its value is male and we want to show only those checkboxes selected for which the value is true
in the skills
object.
Therefore, for the defaultValues
option, we're looping over the skills
object using the filter
method to find out the skills
for which the value is true
as shown below:
const {
register,
handleSubmit,
formState: { errors }
} = useForm({
defaultValues: {
gender: initialValues.gender,
skills: Object.keys(initialValues.skills).filter(
(item) => initialValues.skills[item] === true
)
}
});
As the JavaScript
and nodejs
values are true
, the skills
array after the filter
method will become ["JavaScript", "nodejs"]
so the defaultValues
object will look like this:
useForm({
defaultValues: {
gender: 'male',
skills: ["JavaScript", "nodejs"]
}
});
Therefore, when the page is loaded, only the male
gender and the JavaScript
and Node.js
skills will be selected/checked by default.
Note that the casing used in the skills
object has to match with the value
specified for the checkbox.
So even though the label for checkbox is Node.js
, it's value is nodejs
so we're using nodejs
as the key in the initialValues
object.
Below is the demo of how it looks on page load.
And that's it! You've learned how to use react-hook-form to more easily build complex forms in React.
Thanks for Reading!
If you want to learn Redux in detail from scratch and build 3 apps along with the complete food ordering app, check out my Mastering Redux course.
In the course, you will learn:
- Basic and advanced Redux
- How to manage the complex state of array and objects
- How to use multiple reducers to manage complex redux state
- How to debug a Redux application
- How to use Redux in React using the react-redux library to make your app reactive.
- How to use the redux-thunk library to handle async API calls
- Build 3 different apps using Redux
and much more.
Finally, we'll build a complete food ordering app from scratch with stripe integration for accepting payments and deploy it to production.
Want to stay up to date with regular content regarding JavaScript, React, Node.js? Follow me on LinkedIn.
Top comments (2)
Good article, very thorough! So much shallow stuff on dev.to these days, but this is a solid post, I liked it.
Glad to hear that. Thank you🙏