Introduction
Hi, guys!
Recently, I spent some time improving my React skills. And the feeling that I will never know enough never goes away, but that is another talk. Just know it is perfectly normal to feel this way.
This is how I found out about React Hook Form. Ok, actually, it wasn't first time I heard about it, but now I just decided to learn more about it. So I just read their docs a little bit and decided I will share the knowledge with you.
I won't dive too deep into the subject. The goal is to get as familiar as possible with this library.
So let's get started!
What is React Hook Form and Why Should You Use It?
It is a light library that helps you manage forms easily in React.
What it offers:
Light
Provides an intuitive experience, with rich features, all of these while staying light and not relying on other dependencies.
Performance
It reduces the amount of code, it reduces the number of re-renders, due to its use of uncontrolled components by default and offers faster mounting and reduced validation computations. Because the form components don't update the state with every keystroke, the components and its children don't re-render.
Validations
React Hook Form leverages existing HTML markup and makes form validation easy.
Error Handling
It provides an errors object which you can use to retrieve errors.
Loading State
Methods like isSubmitting, isLoading, isValidating makes managing loading state more intuitive.
Installation and Basic Usage
React Hook Form is easy to setup. Start by using the following command in your terminal:
npm install react-hook-form
Imagine how you do it normally without React Hook Form:
import { useState } from 'react';
const Form = () => {
const [ name, setName ] = useState('');
return (
<form>
<input onChange={() => setName(e.target.value)} value={name} />
</form>
);
};
This is fine, right? It is a very small form, won't create any problems, hopefully. But what if it scales? What if instead of one input you get 10? Imagine you have to manage that huge form state, together with loading and error handling. And think about performance and the amount of code written. The number of re-renders will be huge, maybe the app will be sluggish.
And this is how it comes to the rescue. Let's demonstrate some basic usage:
import { useForm } from "react-hook-form";
export default function App() {
const { register, handleSubmit } = useForm();
const onSubmit = (data) => console.log(data);
return (
/* "handleSubmit" will validate inputs before invoking "onSubmit" */
<form onSubmit={handleSubmit(onSubmit)}>
{/* register your input into the hook by invoking the "register" function */}
<input {...register("example")} />
<input type="submit" />
</form>
);
}
When you call useForm(), the function returns a number of useful functions. We will talk about some of them in the next sections, but two of the most important ones are register and handleSubmit.
register function, as the name implies, will register your input into the hook and will apply validation rules to it. What does it mean? It means you now don't need useState and onChange and value to manage your input state. It is managed by React Hook Form, but leveraging uncontrolled components.
When you actually use it inside an input, the syntax is this:
<input {...register("name"), validations} />
It accepts two parameters. The first one will be a string and it's going to be a name of your choosing. Keep in mind that this is like applying a name attribute on a HTML tag and you are going to use this name with other useForm() functions as well. The second attribute is an object with validations, which we'll discuss in the next section. So you could have something like this:
import { useForm } from "react-hook-form";
export default function App() {
const { register, handleSubmit } = useForm();
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName", { reguired: true, maxLength: 20 })} />
<input type="submit" />
</form>
);
}
handleSubmit will receive your form data only if input validation ended successfuly. It accepts two callbacks, one for success and one for errors.
import { useForm } from "react-hook-form";
export default function App() {
const { register, handleSubmit } = useForm();
const onSubmit = (data) => console.log(data);
const onError = (errors) => console.log(errors);
return (
<form onSubmit={handleSubmit(onSubmit, onError)}>
<input {...register("firstName")} />
<input {...register("lastName")} />
<input type="email" {...register("email")} />
<input type="submit" />
</form>
);
}
Validations, Error Handling and Loading
React Hook Form aligns with HTML standard for form validations. This means it supports rules like:
- required
- min
- max
- minLength
- maxLength
- pattern
- validate
Let's see how it works:
import { useForm } from "react-hook-form";
export default function App() {
const { register, handleSubmit } = useForm();
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName", { reguired: true, maxLength: 20, minLength: 1 })} />
<input type="number" {...register("age", { required: true, min: 18, max: 99 })} />
<input type="number" {...register("experience"), { pattern: /\d/ }}
<input type="number" {...register("lastName"), {
validate: { (lastName) => if (!lastName) => 'Last name is required' }
}}
<input type="submit" />
</form>
);
}
While some of them are naturally logical and intuitive, the validate rule asks for some explanations. It is an object where you can insert as many validation methods (functions) as you need. For example:
register('fieldName', {
validate: {
// 1. First Validation Function
mustBeAdult: (value) => value >= 18 || "Must be 18 or older",
// 2. Second Validation Function
isEvenNumber: (value) => value % 2 === 0 || "Must be an even number",
// 3. Third Validation Function
notZero: (value) => value !== 0 || "Cannot be zero",
},
});
How does error handling work with validate rule? The validation functions run in the order they are defined and if a validation function returns true, validation passes for that rule.
If a function returns a string like "Must be 18 or older", that string is treated as the error message, and validation stops for that field immediately and will display the error message from the first function that fails.
There are other rules for validations inside register function as well. Read more about them here.
To handle errors in React Hook Form is easy. You first have to destructure the useForm() hook and get the formState, which you will then destructure to get errors:
import { useForm } from 'react-hook-form';
const Form = () => {
const { register, handleSubmit, formState: {errors} } = useForm();
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName", { reguired: 'Name required', maxLength: { value: 20, message: 'No more than 20' }} )} />
{errors.firstName.message && <p>{errors}</p>}
<input type="number" {...register("age", { required: true, min: { value: 18, message: 'At least 18' }, max: 99 })} />
{errors.age.message && <p>{errors}</p>}
<input type="submit" />
</form>
);
}
The syntax for writing errors is something like this:
{...register("age", { required: true, min: { value: 18, message: 'At least 18' }})}
And instead of required: true, you can also give this property a string value, which will act as an error message. Now if the field is not filled you will see 'Name required' under it. Notice that when you register your input, the errors object will get a key that will match the name you registered the input with. And that key will be an object which will hold the 'message' property. That is how you can smartly access the errors for every field. No headache required. So in our case:
errors.firstName.message
errors.age.message
The formState also holds other useful properties. Check them all here. We'll also discuss here some of them that are related to loading state: isSubmitting, isLoading, isValidating.
All of these three are booleans. And you guessed right, isSubmitting is true during the time the form is submitting. You might use it when you want to disable a submit button during form submission. isLoading only works with default async values and it is true when the form is loading these values. Example:
const { formState: { isLoading, isSubmitting } } = useForm({
defaultValues: async () => await fetch('/api');
})
We didn't say anything about default values in useForm as props yet. But it is easy to understand. It's like using defaultValue in React. However, as the documentation recommends:
The defaultValues prop populates the entire form with default values. It supports both synchronous and asynchronous assignment of default values. While you can set an input's default value using defaultValue or defaultChecked (as detailed in the official React documentation), it is recommended to use defaultValues for the entire form.
So you will use isLoading if you have default values that are async, in other words, if those values, that you want as defaults for your inputs, take time until they are loaded into your form.
The last one, isValidating is going to be true during validation, otherwise false. Example for all:
import { useForm } from "react-hook-form";
import { useState } from 'react';
export default function App() {
const [ value, setValue ] = useState();
const { register, handleSubmit, formState: {errors, isSubmitting, isLoading, isValidating},
} = useForm({
defaultValues: async () => await someValue().then((value) => setValue(value));
});
const onSubmit = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
{isValidating? <p>Validating your form...</p> : <p>Validated successfuly!</p>}
{isLoading? <p>The value is loading</p> : <p>{value}</p>}
<input {...register("test")} />
<button disabled={isSubmitting? true : false}>{isSubmitting? 'Submitting...' : 'Submit'}</button>
</form>
);
}
UseForm Functions and Props
When you call useForm(), you make available a useful set of properties and methods. Some of them we already discussed, like register, formState, handleSubmit. Some others are unregister, setErrors, watch, setValue, reset, setFocus. And there are other more. But let's say a thing or two about these, for now, just to get the hang of it.
You will want to use unregister when you need to programmatically remove a field's data and validation rules from the form state. Imagine you have some fields in your app that are conditionally rendered. Imagine a field that is not rendered if a checkbox is not checked, being conditionally rendered. Without unregistering this field, even if the field is not there visually, it exists as state and validation. When you submit the form, the form is also submitted with data from this field, that you didn't even use. You can expect some problems in your app if you don't unregister.
You can do this programatically using useEffect. This is a cleanup that will run whenever the component will unmount. So this how you remove the field's state and data.
const { unregister } = useForm();
useEffect(() => {
return () => {
unregister("fieldName");
};
}, [unregister]);
However, there is another approach, a more modern one, that implies enabling automatic unregistration:
Just like using defaultValues prop, this is another prop in useForm that clean up the field state when the component unmounts and makes your code much cleaner. Note that this is false by default.
const methods = useForm({
// Set this option to true
shouldUnregister: true,
});
The setErrors and setValue are both valuable when you need to programatically set other errors that are maybe not validation related, such as network errors or so and when you want to set a value, when clicking a button, for example.
const { unregister } = useForm();
// pretend it's a network error
if (networkError) {
setError('name of the field', { type: string, message: 'Network error'});
}
// pretend you clicked a button to change an input value
if (buttonClicked) {
setValue('name of the field', value);
}
The reset function allows you to reset the whole form state. It accepts options which allow for partial reset. setFocus allow to set focus on a field programatically. It accept a string value, which is going to be the name of the field you want to focus on and some options as an object.
The watch function lets you subscribe to input changes. Keep in mind that this will activate re-rendering. You can watch a sinlge field, multiple fields of your choice or the entire form.
// single field
const form = watch();
// multiple fields
const names = watch([ 'firstName', 'lastName' ]);
// entire form
const name = watch("name");
The useForm() accepts props. We have already seen some of them earlier, like defaultValues, shouldUnsubscribe. But there are many others: disabled, errors, values. And also there some specific ones that are related to validation libraries, like zod and yup, which are called resolvers. You can read more about them here.
Implementation with Zod + TypeScript
We have seen that React Hook Form also accepts validations by default. If you want to go even further, you can make use of Zod and TypeScript. These are modern ways of validating user inputs and make your app more robust.
You can create a Zod schema and apply it to the hook. This is how:
First you have to install the resolvers.
npm i @hookform/resolvers
Then you import it, create the schema and use it in the props of useForm() hook;
import { zodResolver } from '@hookform/resolvers/zod;
interface schema {
firstName: string,
lastName: string
}
const { register, handleSubmit } = useForm<schema>({
resolver: zodResolver(schema)
});
This will validate your input against the schema and return with either errors or a valid result.
React Hook Form doesn't only support Zod. It also supports schema-based form validation with Yup, Superstruct or Joi.
Conclusion
I hope I made React Form Hook at least a bit more easier to start with. Now you've got the basics down... Next step is to go a little deeper according to your needs.
Check out the official documentation
Connect with me
I am open for work, for collaborations, for freelancing or just tech related discussions.
So for anything, just let me know, leave me a message and I'll get in touch as soon as possible!
- Write me an Email: alexandru.ene.dev@gmail.com
- Follow me and see my work on Github: alexandru-ene-dev
- Let's be friends on: LinkedIn: alexandru-ene-dev
- Let's chat on Discord: alexandru.ene.dev
- Find me also on Dev.to: alexandru-ene-dev
Thank you so much again for reading and I will see you in the next one soon! Happy coding! :)
Top comments (0)