How to build a React form component

Allie Beazell
Allie Beazell
Retool

Aug 13, 2020

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

How to start building your React 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 get register and handleSubmit().

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.


How to style your React form with CSS

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!

Using a React component library

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.

Validate your React form component

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

Making all fields required

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).

Adding an address validator

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.

Validating date

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!

Validating order number

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.

Communicate errors in your React form component

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.

Adding an address error

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:

Adding an order number error

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.

Your React form component with react-hook-form

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.

TL;DR: Syntax roundup

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:

Create a simple React form component

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

Style your React form component

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

Validate your React form component

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

Add errors to your React form component

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

Full form

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

Other React form libraries

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 with redux-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.

Allie Beazell
Allie Beazell
Retool
Aug 13, 2020
Copied