Whether it's a login page or an internal tool, your React app is going to need a form, and handling events and dataflow via raw HTML inputs isn't any fun. This guide will walk you through how to use the react-hook-form
library and take you step-by-step through a project where we create a form for an internal tool and extend it with some useful features.
By the end of this article, you’ll know how to:
- Create a simple form using
react-hook-form
- Style your form
- Validate your form
- Add errors to your form
For this tutorial, we're working with a table that lists and orders our data, and has a nifty datepicker for sifting through the orders.
Now, while we know most folks place orders online, we have to recognize that sometimes customers like to order over the phone. This means that we need to give our reps the ability to add new orders to the table.
Our React form component needs to be able to:
- Accept a customer’s name, address, the date the order was made, and an order number
- Validate the data that the customer support rep enters
- Display errors to the rep
Here is what the final product will look and feel like:
First things first, react-hook-form
is a library built to handle the data in forms and do all the complicated work with validation, error handling, and submitting. There are no physical components in the library. The form component that we will build will just be made with standard jsx
tags.
To start off, we’re going to build a simple form with no styling - it’s going to be a bunch of textarea
inputs for the reps to fill out the customer’s name, address, the date of the order, and the order number, and, finally, a plain “submit” button. Keep in mind that react-hook-form
uses React Hooks. Hooks are a fairly new feature to React, so if you aren’t familiar, we recommend checking out React’s Hooks at a Glance docs before starting this tutorial.
After you import the useForm()
hook, there are basic steps to run through:
- Use the
useForm()
hook to getregister
andhandleSubmit()
.
You need to pass register
into the ref
prop when you create your form so the values the user adds—and your validation rules—can be submitted. Later on in this tutorial, we will use register
to handle validation. handleSubmit()
for onSubmit
connects your actual form into react-hook-form
(which provides register in the first place).
1const { register, handleSubmit } = useForm();
2
- Create a function to handle your data, so your data actually winds up in your database
Your backend is your own, but we’re going to pretend that we have a saveData()
function in another file that handles saving our data to a database. It’s just console.log(data)
for the purposes of this tutorial.
- Render your form
We’re creating a React form component, so we will use form-related jsx
tags to build it, like <form>
, <h1>
, <label>
, and <input>
Let’s start with a <form>
container. Be sure to pass your saveData()
function into react-hook-form
’s handleSubmit()
that you got from the useForm()
hook and then into the onSubmit()
in the <form>
tag. If that sounded really confusing, peep the code below:
1<form onSubmit={handleSubmit(data => saveData(data))}>
2 ...
3</form>
4
Next, let’s add a header with <h1>
so our reps know what this form is for:
1<form ...>
2 <h1>New Order</h1>
3</form>
4
We’re going to create four <label>
and <input>
pairs for name, address, date, and order number. For each <input>
, be sure to pass register
from the useForm()
hook into the ref
prop and give it a name in the name prop.
1<label>Name</label>
2<input name="name" ref={register} />
3<label>Address</label>
4<input name="address" ref={register} />
5<label>Date</label>
6<input name="date" ref={register} />
7<label>Order Number</label>
8<input name="order" ref={register} />
9
Finally, we’ll add a submit button by using an <input>
with a “submit” type:
1<input type="submit" />
2
Putting it all together, we will have the following:
1import React from "react";
2import { useForm } from "react-hook-form";
3
4import saveData from "./some_other_file";
5
6export default function Form() {
7 const { register, handleSubmit } = useForm();
8
9 return (
10 <form onSubmit={handleSubmit(data => saveData(data))}>
11 <h1>New Order</h1>
12 <label>Name</label>
13 <input name="name" ref={register} />
14 <label>Address</label>
15 <input name="address" ref={register} />
16 <label>Date</label>
17 <input name="date" ref={register} />
18 <label>Order Number</label>
19 <input name="order" ref={register} />
20 <input type="submit" />
21 </form>
22 );
23}
24
Which will look like this:
Cool, now we have a (kinda) working form.
Subscribe to the Retool monthly newsletter
Once a month, we send out top stories (like this one) along with Retool tutorials, templates, and product releases.
You can easily style your form with CSS modules, styled-components
, or your favorite kind of styling. For our tutorial, we’re going to use styled-components
.
First, we install and import style-components
into our project. Then, we create a styled component (based on a <div>
) and plop all of our pretty CSS into that. Finally, we wrap our form in the <Styles>
tag to apply the styles. Easy!
1import React from "react";
2import { useForm } from "react-hook-form";
3import styled from "styled-components";
4
5import saveData from "./some_other_file";
6
7const Styles = styled.div`
8 background: lavender;
9 padding: 20px;
10
11 h1 {
12 border-bottom: 1px solid white;
13 color: #3d3d3d;
14 font-family: sans-serif;
15 font-size: 20px;
16 font-weight: 600;
17 line-height: 24px;
18 padding: 10px;
19 text-align: center;
20 }
21
22 form {
23 background: white;
24 border: 1px solid #dedede;
25 display: flex;
26 flex-direction: column;
27 justify-content: space-around;
28 margin: 0 auto;
29 max-width: 500px;
30 padding: 30px 50px;
31 }
32
33 input {
34 border: 1px solid #d9d9d9;
35 border-radius: 4px;
36 box-sizing: border-box;
37 padding: 10px;
38 width: 100%;
39 }
40
41 label {
42 color: #3d3d3d;
43 display: block;
44 font-family: sans-serif;
45 font-size: 14px;
46 font-weight: 500;
47 margin-bottom: 5px;
48 }
49
50 .error {
51 color: red;
52 font-family: sans-serif;
53 font-size: 12px;
54 height: 30px;
55 }
56
57 .submitButton {
58 background-color: #6976d9;
59 color: white;
60 font-family: sans-serif;
61 font-size: 14px;
62 margin: 20px 0px;
63`;
64
65function Form() {
66 const { register, handleSubmit } = useForm();
67
68 return (
69 <form onSubmit={handleSubmit(data => saveData(data))}>
70 <label>Name</label>
71 <input name="name" ref={register} />
72 <label>Address</label>
73 <input name="address" ref={register} />
74 <label>Date</label>
75 <input name="date" ref={register} />
76 <label>Order Number</label>
77 <input name="order" ref={register} />
78 <input type="submit" className="submitButton" />
79 </form>
80 );
81}
82
83export default function App() {
84 return (
85 <Styles>
86 <Form />
87 </Styles>
88 );
89}
90
That’s a lot of styling code, but look where it gets us!
If you hate battling CSS, using a React component library might be a good option. It can add a lot of functionality, like animations, that are time-consuming to implement. If you’re not familiar with the plethora of React component libraries, you can check out our recent post that covers our favorites. For this example, we’re going to use Material UI.
The easiest way to incorporate a React component library is to use one that exposes the ref
field as a prop. Then, all you have to do is substitute it for the <input>
field and then pass register
to that ref.
1import { Button, TextField } from "@material-ui/core";
2
3...
4
5function Form() {
6 const { register, handleSubmit } = useForm();
7
8 return (
9 <>
10 <h1>New Order</h1>
11 <form onSubmit={handleSubmit(data => saveData(data))}>
12 <label>Name</label>
13 <TextField name="name" inputRef={register} />
14 ...
15 // Let's use Material UI's Button too
16 <Button variant="contained" color="primary">Submit</Button>
17 </form>
18 </>
19 );
20}
21
Now, we get the sleekness and functionality of Material-UI.
The last thing we want is for our customer support reps to add faulty data into our database. If we have any other apps using the same data, like reports running on the number of orders made in a certain time span, then adding in a date that isn’t formatted correctly could ruin the whole thing.
For our use case, we are going to add validation in the form of:
- Making all fields required
- Adding an address validator
- Validating date
- Validating order number
All you have to do to make a field required is pass an object into the register()
prop in input that says {required: true}
.
1<input name="name" ref={register({ required: true })} />
2
This will flag the errors
prop for the “name” field, which can then be used to add an error message (see next section).
To make our life easy, we are going to add a validator to check whether the address the user enters exists and is properly formatted.We’ll use a mock function from our example and show you how to integrate it into the React form component.
First, we define our validator function. For our purposes, we are just checking a specific string. This is where you would hook into your validator library.
1function addressValidator(address) {
2 if (address === "123 1st St., New York, NY") {
3 return true;
4 }
5 return false;
6}
7
Next, we add validation to the register for address input. Make sure to pass the “value” that the user enters. If your validator function returns true, then it is validated and no error will appear.
1<input name="address" ref={register({
2 required: true,
3 validate: value => addressValidator(value),
4})} />
5
If you want to go further with your address validation than just adding a mock function (which you probably do because this is useless in production), we recommend checking out this awesome tutorial from HERE on validating location data.
To make sure users only enter valid dates into our date input field, we're going to add type="date"
to our date input field in the React form component in order to force the user to fill out the field in our specified format.
In some browsers (like Chrome), this will add a DatePicker to the input box. In all browsers, it will provide a clear format for the date the rep should enter and will not let them use a different format. We can even add a max date to stop the customer support rep from accidentally adding a future order date (as much as we’d all love to just skip 2020).
For this section, we’re going to use the moment
library since it makes formatting dates much easier than JavaScript’s native date.
1import moment from 'moment';
2
3...
4<input
5 name="date"
6 type="date"
7 max={moment().format("YYYY-MM-DD")}
8 ref={register({ required: true })}
9/>
10
The cool thing about validating the date in the input as opposed to the register is that we won’t have to waste time and energy building out error messages since the input will stop our user from entering an erroneous value.
Looking good!
For our order number field, we need to add validation that ensures the input is a valid order number in our system. react-hook-form
has a really easy way to apply regex validation by passing a “pattern” into the register.
Let’s say that our order numbers are always 14 integers long (though this regex could easily be updated to fit whatever your order numbers look like).
1<input
2 name="order"
3 ref={register({
4 required: true,
5 minLength: 14,
6 maxLength: 14,
7 pattern: /\d{14}/,
8 })}
9/>
10
Great work! Now an error will bubble up when the order number does not meet our specified pattern. For more details, you can read more in the register
section of the react-hook-form
documentation.
Adding error handling to your form is easy with react-hook-form
. Let’s start with communicating that certain fields are required. All we have to do is get errors
from the useForm()
hook and then add a conditional to render them under the input if they are needed.
1function Form() {
2 const { register, errors, handleSubmit } = useForm();
3
4 return (
5 <form onSubmit={handleSubmit(data => saveData(data))}>
6 <h1>New Order</h1>
7 <label>Name</label>
8 <input name="name" ref={register({ required: true })} />
9 {errors.name && "Required"}
10 <label>Address</label>
11 <input
12 name="address"
13 ref={register({
14 required: true,
15 validate: value => addressValidator(value)
16 })}
17 />
18 {errors.address && "Required"}
19 <label>Date</label>
20 <input
21 name="date"
22 type="date"
23 max={moment().format("YYYY-MM-DD")}
24 ref={register({ required: true })}
25 />
26 {errors.date && "Required"}
27 <label>Order Number</label>
28 <input
29 name="order"
30 ref={register({
31 required: true,
32 pattern: /\d{14}/,
33 })}
34 />
35 {errors.order && "Required"}
36 <input type="submit" />
37 </form>
38 );
39}
40
Notice how we refer to the error for a specific input field by using errors.name
and errors.date
. And here is what our error looks like:
One last issue - since these errors are conditionals, they’ll increase the size of our form. To get around this, we will make a simple error component that renders the height of the error, even if there is no text. We’ll also color the text red, so it’s easier to see.
1import React from "react";
2import { useForm } from "react-hook-form";
3import styled from "styled-components";
4
5import saveData from "./some_other_file";
6
7const Styles = styled.div`
8 background: lavender;
9 ...
10 .error {
11 color: red;
12 font-family: sans-serif;
13 font-size: 12px;
14 height: 30px;
15 }
16`;
17
18// Render " " if no errors, or error message if errors
19export function Error({ errors }) {
20 return <div className={"error"}>{errors ? errors.message : " "}</div>;
21}
22
23export function Form() {
24 const { register, handleSubmit } = useForm();
25
26 return (
27 <form onSubmit={handleSubmit(data => saveData(data))}>
28 <h1>New Order</h1>
29 <label>Name</label>
30 <input name="name" ref={register({ required: true })} />
31 <Error errors={errors.name} />
32 <label>Address</label>
33 <input
34 name="address"
35 ref={register({
36 required: true,
37 validate: value => addressValidator(value)
38 })}
39 />
40 <Error errors={errors.address} />
41 <label>Date</label>
42 <input
43 name="date"
44 type="date"
45 max={moment().format("YYYY-MM-DD")}
46 ref={register({ required: true })}
47 />
48 <Error errors={errors.date} />
49 <label>Order Number</label>
50 <input
51 name="order"
52 ref={register({
53 required: true,
54 pattern: /\d{14}/,
55 })}
56 />
57 <Error errors={errors.order} />
58 <input type="submit" className="submitButton" />
59 </form>
60 );
61}
62...
63
But wait! There’s no error message text to render. To fix this, let’s start with the Required validation. We do this by adding the error message for that particular type of error.
1<input name="name" ref={register({ required: 'Required' })} />
2
Go through your code and change required: true
to required: 'Required'
in every place that you see it. Now this functions a lot more like a form we would expect to see in the real world:
But hold on! We validated a lot more than just required fields. Let’s get a little more granular with these errors, so our customer support reps know how to fix the problem.
To add an address error to your validate
section, simply add an ||
so that if your validation function returns “false,” it will display your message instead.
1<input
2 name="address"
3 ref={register({
4 required: 'Required',
5 validate: value => addressValidator(value) || 'Invalid address',
6 })}
7/>
8
Here is what your error will look like:
In our system, our order numbers are always 14 digits long and made up of positive integers between 0-9. To verify this order number pattern, we are going to use minLength
and maxLength
to verify length and pattern
to verify the pattern.
First, change “minLength”, “maxLength”, and “pattern” into objects with a value key, where the regex pattern or number you defined is the value, and a message
key, where the value is your error message.
1<input
2 name="order"
3 ref={register({
4 required: 'Required',
5 minLength: {
6 value: 14,
7 message: 'Order number too short',
8 },
9 maxLength: {
10 value: 14,
11 message: 'Order number too long',
12 },
13 pattern: {
14 value: /\d{14}/,
15 message: "Invalid order number",
16 },
17 })}
18/>
19
Here is what your error will look like:
And that’s it for errors! Check out react-hook-form
’s API docs for more information.
Here is our final React form component:
For more code samples that cover the vast range of features that react-hook-form has to offer, check out React Hook Form’s website. And for a full version of this code that you can test out and play around with, check out our code sandbox.
We know that this tutorial covered a ton of features for forms in react-hook-form
, so just to make sure you didn’t miss anything, here is a roundup of the features we covered:
1import React from "react";
2import { useForm } from "react-hook-form";
3
4import saveData from "./some-other-file";
5
6export default function Form() {
7 const { register, handleSubmit } = useForm();
8
9 return (
10 <form onSubmit={handleSubmit(data => saveData(data))}>
11 <label>Field</label>
12 <input name="field" ref={register} />
13 <input type="submit" />
14 </form>
15 );
16}
17
1import React from "react";
2import { useForm } from "react-hook-form";
3import styled from "styled-components";
4
5import saveData from "./some_other_file";
6
7const Styles = styled.div`
8background: lavender;
9 padding: 20px;
10
11 h1 {
12 border-bottom: 1px solid white;
13 color: #3d3d3d;
14 font-family: sans-serif;
15 font-size: 20px;
16 font-weight: 600;
17 line-height: 24px;
18 padding: 10px;
19 text-align: center;
20 }
21
22 form {
23 background: white;
24 border: 1px solid #dedede;
25 display: flex;
26 flex-direction: column;
27 justify-content: space-around;
28 margin: 0 auto;
29 max-width: 500px;
30 padding: 30px 50px;
31 }
32
33 input {
34 border: 1px solid #d9d9d9;
35 border-radius: 4px;
36 box-sizing: border-box;
37 padding: 10px;
38 width: 100%;
39 }
40
41 label {
42 color: #3d3d3d;
43 display: block;
44 font-family: sans-serif;
45 font-size: 14px;
46 font-weight: 500;
47 margin-bottom: 5px;
48 }
49
50 .submitButton {
51 background-color: #6976d9;
52 color: white;
53 font-family: sans-serif;
54 font-size: 14px;
55 margin: 20px 0px;
56 }
57`;
58
59export function Form() {
60 const { register, handleSubmit } = useForm();
61
62 return (
63 <form onSubmit={handleSubmit(data => saveData(data))}>
64 <label>Field</label>
65 <input name="field" ref={register} />
66 <input type="submit" className="submitButton" />
67 </form>
68 );
69}
70
71export default function App() {
72 return (
73 <Styles>
74 <Form />
75 </Styles>
76 );
77}
78
1<form onSubmit={handleSubmit(data => saveData(data))}>
2 <label>Name</label>
3 <input name="name" ref={register({ required: true })} />
4 <label>Address</label>
5 <input
6 name="address"
7 ref={register({
8 required: true,
9 validate: value => addressValidator(value)
10 })}
11 />
12 <label>Date</label>
13 <input
14 name="date"
15 type="date"
16 max={moment().format("YYYY-MM-DD")}
17 ref={register({ required: true })}
18 />
19 <label>Order Number</label>
20 <input
21 name="order"
22 ref={register({
23 required: true,
24 pattern: /\d{14}/,
25 })}
26 />
27 <input type="submit" />
28</form>
29
1export default function Form() {
2 const { register, errors, handleSubmit } = useForm();
3
4 return (
5 <form onSubmit={handleSubmit(data => saveData(data))}>
6 <label>Field</label>
7 <input name="field" ref={register({ required: true })} />
8 {errors.name && "Name is required"}
9 </form>
10 );
11}
12
1import React from "react";
2import { useForm } from "react-hook-form";
3import styled from "styled-components";
4import moment from 'moment';
5
6import saveData from "./some_other_file";
7
8const Styles = styled.div`
9 background: lavender;
10 padding: 20px;
11
12 h1 {
13 border-bottom: 1px solid white;
14 color: #3d3d3d;
15 font-family: sans-serif;
16 font-size: 20px;
17 font-weight: 600;
18 line-height: 24px;
19 padding: 10px;
20 text-align: center;
21 }
22
23 form {
24 background: white;
25 border: 1px solid #dedede;
26 display: flex;
27 flex-direction: column;
28 justify-content: space-around;
29 margin: 0 auto;
30 max-width: 500px;
31 padding: 30px 50px;
32 }
33
34 input {
35 border: 1px solid #d9d9d9;
36 border-radius: 4px;
37 box-sizing: border-box;
38 padding: 10px;
39 width: 100%;
40 }
41
42 label {
43 color: #3d3d3d;
44 display: block;
45 font-family: sans-serif;
46 font-size: 14px;
47 font-weight: 500;
48 margin-bottom: 5px;
49 }
50
51 .error {
52 color: red;
53 font-family: sans-serif;
54 font-size: 12px;
55 height: 30px;
56 }
57
58 .submitButton {
59 background-color: #6976d9;
60 color: white;
61 font-family: sans-serif;
62 font-size: 14px;
63 margin: 20px 0px;
64 }
65`;
66
67export function addressValidator(address) {
68 if (address === "123 1st St., New York, NY") {
69 return true;
70 }
71 return false;
72}
73
74export function Error({ errors }) {
75 return <div className={"error"}>{errors ? errors.message : " "}</div>;
76}
77
78export function Form() {
79 const { register, errors, handleSubmit } = useForm();
80
81 return (
82 <form onSubmit={handleSubmit(data => saveData(data))}>
83 <h1>New Order</h1>
84 <label>Name</label>
85 <input name="name" ref={register({ required: 'Required' })} />
86 <Error errors={errors.name} />
87 <label>Address</label>
88 <input
89 name="address"
90 ref={register({
91 required: 'Required',
92 validate: value => addressValidator(value) || 'Invalid address',
93 })}
94 />
95 <Error errors={errors.address} />
96 <label>Date</label>
97 <input
98 name="date"
99 type="date"
100 max={moment().format("YYYY-MM-DD")}
101 ref={register({ required: 'Required' })}
102 />
103 <Error errors={errors.date} />
104 <label>Order Number</label>
105 <input
106 name="order"
107 ref={register({
108 required: 'Required',
109 minLength: {
110 value: 14,
111 message: 'Order number too short',
112 },
113 maxLength: {
114 value: 14,
115 message: 'Order number too long',
116 },
117 pattern: {
118 value: /\d{14}/,
119 message: "Invalid order number",
120 },
121 })} />
122 <Error errors={errors.order} />
123 <input type="submit" className="submitButton" />
124 </form>
125 );
126}
127
128export default function App() {
129 return (
130 <Styles>
131 <Form />
132 </Styles>
133 );
134}
135
react-hook-form
has nearly 35K stars on GitHub, but it's worth taking a second to explain why we decided to go with react-hook-form
instead of other popular React form libraries, like formik
and react-final-form
. It’s worth recognizing that these form libraries are pretty awesome in their own ways:
formik
has top-notch documentation and extremely thorough tutorials.react-final-form
is great for those used to working withredux-final-form
.
Ultimately, we chose react-hook-form
because it has a tiny bundle size, no dependencies, and is relatively new (many sources, like LogRocket and ITNEXT, are claiming it is the best library for building forms in React) compared to the rest. If you’re interested in learning about some other ways to build React forms, check this out.
Reader