React Hook Form, an easy way to build forms

Bryan Arellano
11 min readOct 12, 2021

When I started learning React I was fascinated with the learning curve because is easy to understand the explanations and code examples, but when I tried to apply that knowledge I faced a problem, forms. Handling forms in React requires you to write lots of boilerplate code for:

  • Managing and validating user input values with state
  • Keeping track of invalid error messages
  • Handling form submission
export const CustomForm = (props) => {    return (
<form className="demoForm">
<h2>Sign up</h2>
<div className="panel panel-default">
<FormErrors formErrors={this.state.formErrors}/>
</div>
<div className={`form-group ${this.errorClass(this.state.formErrors.email)}`}>
<label htmlFor="email">Email address</label>
<input type="email" required className="form-control" name="email"
placeholder="Email"
value=
{this.state.email}
onChange={this.handleUserInput}/>
</div>
<div className={`form-group ${this.errorClass(this.state.formErrors.password)}`}>
<label htmlFor="password">Password</label>
<input type="password" className="form-control" name="password"
placeholder="Password"
value=
{this.state.password}
onChange={this.handleUserInput}/>
</div>
<button type="submit" className="btn btn-primary" disabled={!this.state.formValid}>Sign up</button>
</form>
);
}

In this code we can see a validation where a handler is placed in onChange event to each field, a state value is placed to validate the form state.formValid , and to track errors uses another value from the state state.formErrors. I saw this kind of example many times, and is not bad but is awful and untenable when you have many fields or many forms and you need to validate many values. But luckily the developers have created libraries to help to build forms with React easily, for example, React Hook Form or Formik are the perfect examples, and I’m going to talk about React Hook Form and my experience with that library.

When I started to work with React Hook Form I was afraid, because I didn’t understand how it worked and how I should implement it. The documentation was there with basic examples, but when you need to implement this kind of libraries with patterns or a good design in your code the process is a little harder. But don’t worry, I’m going to try to explain some good practices and tips that I found. I built a project with its implementation, and I used stateless(components where you only show data) and stateful components(components where you manipulate the data), redux (global state), and AXIOS (make requests to APIs), the reason is I want to show an example that applies many technologies that may be you are going to use in the future or maybe you use right now.

Before starting with the explanation I going to talk about some terms and the project’s structure.

When you are working with React you can choose the way in which you want to build your application, you have the freedom to work as you want. But a personal recommendation is always to learn a way to build applications with standards, personally, I prefer to work with stateful and stateless components, but what is that? These kinds of components are known as: “Container and Presentational components” or “Smart and Dumb components”, the reason is that you have a main component where you manipulate and track the data (it means each container has a state) and a bunch of reusable components where you only show this data.

When you explore the code, maybe you see a function called useEffect, well in React exists Hooks, basically, a hook allows you to use state and other React features without writing a class. Hooks are the functions which “hook into” React state and lifecycle features from function components. In this case, useEffect listen to changes in a component, for example, if the component was mounted or updated, also you can listen to changes in a specific prop. This hook replaces old functions like componentDidMount, componentDidUpdated and componentWillUnmount.

Another important thing is global’s state manipulation, to do that I used Redux, basically, this library is a way to manage the “state” or we can say it is a cache or storage that can be accessed by all components in a structured way, in this case, I used it to make HTTP requests and store the responses. You can use libraries like Flux or Mobx, but Redux is easier.

Finally but not less important, I used a local backend to return custom responses, I this case I used an application built with Django, I going to share the project, but you can use your own backend. So, if you see in the environment file a prop called URL, don’t worry just replace it with your own URL.

Now the project’s structure has three important folders ‘containers where stateful components are located, ‘components to add our stateless components, and ‘store where we are going to locate the global state, it means that this is the place to locate the logic to make HTTP requests and manipulate the data to use in our components; commons folder going to store custom messages for errors in the form, constants for enum files and environment to define an environment file with for example API URLs.

Well, now is coming to the explanation, first how actually works React Hook Form, well this library uses a design pattern called Observer, which is a pattern of behavior, it means that is responsible for communication between objects, for example, when a field changes the error could be triggered and show a message without the necessity of a handler to listen to changes in that field.

First a brief view of the structure of the containers folder:

Here I defined a container called “person”, where you can find the component state and the main file. The state folder is the place where we define the logic to manipulate the data associated with my “person” definition, and here is where the form definition going to be stored; in the main file the components and connections with the global state going to be defined.

The state looks like this:

import {useForm} from "react-hook-form";
import {useEffect} from "react";


export const PersonState = (props) => {
const methods = useForm({
mode: "all"
})
const handlePersonSubmit = (formData) => {
props.createPerson(formData)
}

useEffect(() => {
props.getPeopleList()
}, [])

useEffect(() => {
if(props.createPersonRequestStatus){
methods.reset(
{
name: "",
birth_year: "",
identification: "",
lastname: ""
}
);
props.getPeopleList()
}
},[ props.createPersonRequestStatus])

return {
methods,
actions: {
handlePersonSubmit
},
}
}

Here are some definitions that are interesting, for example, you can see a parameter called props, well this parameter contains all methods and information from our global state and it is set from our main file. You can see two useEffect hooks, the first one brings data from a custom API, this data is used in a custom component to show a list with the records that we already have, and the second one listens to a change in a specific value and when this value change we reload the data of the list, so we can see the records from our API in real-time. Finally, we return all methods and information that we need to work with our form.

In the beginning, you can see the definition of the form.

const methods = useForm({
mode: "all"
})
const handlePersonSubmit = (formData) => {
props.createPerson(formData)
}

Basically, we use the useForm that is a custom hook for managing forms with ease and provides the methods to manipulate its state, which means that handles the process of receiving inputs, validating values, and submitting the form. In this definition, we can set default values for each field, or add another behavior. After the initialization of the form, we need a deconstruction to use the form methods, like submit or errors. Finally, you can see a custom method that handles the form submit, don’t worry you don’t need to pass that attribute in each component.

Now, move on to the main file. Here is where we registered the form and its hooks. Also, we associate the attributes and actions from the global state (Redux) to use in our container state.

The code looks like this:

export const Person = (props) => {
const {
methods,
actions,
errors
} = PersonState(props)

return (
<React.Fragment>
<div className="App">
<header className="App-header">
<h1>React Hook Form Exercise</h1>
<Container fixed>
<FormProvider {...methods} >
<Grid container spacing={2}>
<Grid item xs={12} lg={6}>
<Card>
<PersonForm errors={methods.formState.errors}/>
<ActionButtons onSave={methods.handleSubmit(actions.handlePersonSubmit)}
disabled={props.loading}/>
</Card>
</Grid>
<Grid item xs={12} lg={6}>
<PeopleInformation people={props.people} style={{paddingTop: 10}}/>
</Grid>
</Grid>
</FormProvider>
</Container>
</header>
</div>

</React.Fragment>
)
}

export const mapStateToProps = (state) => (
{
loading: state.loading,
people: state.people,
createPersonRequestStatus: state.createPersonRequestStatus
}
)

export const mapDispatchToProps = (
dispatch
) => (
{
getPeopleList: () => dispatch(getPeopleList()),
createPerson: (payload) => dispatch(createPerson(payload))
}
);

export default connect(mapStateToProps, mapDispatchToProps)(Person);

First, I going to talk about mapStateToProps and mapDispatchToProps, basically, here we establish the connection between our global state and our container, and when we export the component the connection is done and we can access the data and methods from the global state and use them in the container.

Now to define our form we need to initiate the local state and bring the methods that we need, also we need to send the values from the container to the state, which means we pass the values from the global state to the local state to use them.

const {
methods,
actions,
errors
} = PersonState(props)

After, we make a wrap to establish the connection between the form and components that going to have the fields of the form, so isn’t necessary to have just one component with all fields of the form, we can have many components with different fields and all of them going to be associated with the form.

<FormProvider {...methods} >
<Grid container spacing={2}>
<Grid item xs={12} lg={6}>
<Card>
<PersonForm errors={methods.formState.errors}/>
<ActionButtons onSave={methods.handleSubmit(actions.handlePersonSubmit)}
disabled={props.loading}/>
</Card>
</Grid>
<Grid item xs={12} lg={6}>
<PeopleInformation people={props.people} style={{paddingTop: 10}}/>
</Grid>
</Grid>
</FormProvider>

You can see a FormProvider, is our wrapper that establishes the connection and we need to provide it with the form methods that we defined in the local state, it solves the problem where data is passed through the component tree without having to pass props down manually at every level. Inside of this provider, we put all components that going to have some relation with the form, isn’t necessarily put all components that have form fields, we can add components to show information, for example, I added a component called PeopleInformation to show a list with information brought from a custom API. Finally, we need a button to handle the submit event, you can see two actions in the button, the first one is handleSubmit, which means that this method going’s to listen to the submit event of the form, and handlePersonSubmit is our custom method to execute the logic when the submit event is triggered. If you asked what is ActionButtons, well is a custom component where I located my button and it receives as a parameter the submit handler.

<Grid item xs={4} style={{padding: 15}}>
<Box pt={4}>
<Button
disabled=
{props.disabled}
size="large"
fullWidth
variant="contained"
color="primary"
onClick=
{props.onSave}
startIcon={<Save/>}
>
Save
{props.disabled && (
<Box ml={1} display="flex" alignItems="center">
<CircularProgress color="inherit" size={20}/>
</Box>
)}
</Button>
</Box>
</Grid>

And now I’m going to explain how we can define form fields, this fragment of code contains just one field but in the project, you can see more fields.

export const PersonForm = (props) => {
const classes = useStyles();
const { control } = useFormContext()
return (
<React.Fragment>
<CardContent>
<Grid container className={classes.root} spacing={2}>
<Grid item xs={12}>
<Typography> Hello, fill the next information</Typography>
</Grid>
<Grid item xs={12}>
<Divider/>
</Grid>
<Grid item xs={12}>
<FormControl
variant="outlined"
error=
{!!props.errors.name}
fullWidth
>
<Controller
name="name"
control=
{control}
defaultValue=""
rules=
{{required: true}}
render={({field: {onChange, value}}) => (
<TextField
id=
{'name'}
label="Name"
variant="filled"
value=
{value}
onChange={onChange}
/>
)}
/>
<FormHelperText error={!!props.errors.name}>
{!!props.errors.name && getErrorMessage(props.errors.name)}
</FormHelperText>
</FormControl>
</Grid>
</CardContent>
</React.Fragment>
);
}

Well the first thing that we need to do is bring the context of the form, did you remember that we defined a wrapper to establish a connection, well here we are going to use that connection, with the context we can associate fields to the form.

const { control } = useFormContext()

To continue we need to define the field controller, in this case, I used FormControl from Material UI to help me with the styles and show the error messages with FormHelperText, you don’t need this to define the controller is just for style.

<FormControl
variant="outlined"
error=
{!!props.errors.name}
fullWidth
>
<Controller
name="name"
control=
{control}
defaultValue=""
rules=
{{required: true}}=> (
<TextField
id=
{'name'}
label="Name"
variant="filled"
value=
{value}
onChange={onChange}
/>
)}
/>
<FormHelperText error={!!props.errors.name}>
{!!props.errors.name && getErrorMessage(props.errors.name)}
</FormHelperText>
</FormControl>

The controller always needs a name, because is the identifier to track the field value, if you define controllers with the same name you are going to get troubles, be careful with that. In this case, my identifier is “name”, you can see a control , this custom hook allows you to access the form context, useFormContext is intended to be used in deeply nested structures, where it would become inconvenient to pass the context as a prop, which means we don’t need to pass the entire form context, the FormProvider allows a kind of remote communication in nested components. We have two optional attributes defaultValue use that if you need to put an initial value in your field, and rules with this, we can add validations in our field, you can add rules like required, maxLength, minLength, or custom rules like for example to validate an email, in this case, I just added a required rule.

Finally, we have the render attribute, here we add the element that we are going to use, any element that you need for example checks, text fields, switches, etc; and render returns a react element and provides the ability to attach events and value into the element, which means you don’t need to add handlers to listen to changes in your fields, for example, onChange handler, render function provides this handler and we don’t need to worry about how to capture the changes on fields or their values. The render prop is used when you can’t refer to the element directly, in this case, I used Material UI to get custom elements like TextField, but I can’t refer this element directly to the form, and render help us with that. For example, you can register an element using the next code:

const { register } = useForm();<input type="text" ref={register} name="firstName" />

In that case, you can register a field directly, because is a native element from React, but libraries like Material don’t allow to pass the register as a value to the ref prop, and we need to use a controller.

And now, how we can handle the error messages? Well, when we defined the form we deconstructed the error variable from the form state.

const methods = useForm({
mode: "all"
})
const { handleSubmit, formState: { errors }} = methods;

With this variable, we get access to all errors in our form, when an error triggers the value in the object is updated automatically and we can show a message if exists a key with the name of our field.

<FormHelperText error={!!props.errors.name}>
{
!!props.errors.name && getErrorMessage(props.errors.name)
}
</FormHelperText>

In this code, we can see a validation where it’s checked if the object “errors” has the key “name”, if the condition is true an error message appears. To get a custom message we can add a function to map the error and assign a message, in this case, is getErrorMessage.

export const getErrorMessage = (error) => {
switch (error.type) {
case "required":
return ErrorsEnum.REQUIRED_FIELD;
case "maxLength":
return ErrorsEnum.MAX_LENGTH;
default:
return ErrorsEnum.DEFAULT_MESSAGE;
}
}

The function receives an object called error, this object has the information about the error type, according to this type we can return a custom message.

Finally, we get a form like this:

You can see the code of the project here: https://github.com/ridouku/react-hook-forms

And, here is the backend: https://github.com/ridouku/react-hook-forms-backend

More information about:

Questions? Comments? Contact me at ridouku@gmail.com

--

--