React Hook Form Crash Course - Speed Up Writing React Form
In this post i prepared for you a React hook form crash course where you will learn everything that you need to know about this library for everyday development.
The first question is why do we need a library to build forms inside React and what makes React hook form so special. Obviously we can build a form without any libraries but it is quite tedious as we need to implement things like validation, blur, highlighting errors and a lot of other sutff. And these are exactly the same things that we use in any form.
This is why it makes a lot of sense to use a library. There are several popular libraries nowadays for React and one of the most popular is React hook form.
It has all features that you need to implement any kind of forms, it leverages hooks and creators of the library are focused on high performance.
Simple example
Let's start with a simple example.
As you can see here I already prepared for us a registration form with username, email and password. Here is how it looks inside my code.
const Register = () => {
return (
<form className="form">
<div className="field">
<label className="label">Username</label>
<input type="text" className="input" />
</div>
<div className="field">
<label className="label">Email</label>
<input type="text" className="input" />
</div>
<div className="field">
<label className="label">Password</label>
<input type="password" className="input" />
</div>
<div>
<button type="submit" className="button">
Register
</button>
</div>
</form>
);
};
export default Register;
It's just a single component Register
with the form inside and 3 different classes - field, label and input. How we can bind now React hook form to this form?
First of all we must install a library.
npm i react-hooks-form
After this we can import and use useForm
hook.
import {useForm} from "react-hook-form";
const Register = () => {
const {register, handleSubmit} = useForm()
...
}
Now we just need to bind handleSubmit
properly to our form.
const Register = () => {
const onSubmit = data => {
console.log('onSubmit', data)
}
return (
<form className="form" onSubmit={handleSubmit(onSubmit)}>
...
</form>
);
};
export default Register;
As you can see inside onSubmit
we provide a handleSubmit
function from the library and the callback onSubmit
which we created.
Our own onSubmit allow us to know when the form was submitted and with which values.
Now we must register every single field in our form.
const Register = () => {
...
return (
<form className="form" onSubmit={handleSubmit(onSubmit)}>
<div className="field">
<label className="label">Username</label>
<input {...register('username')} type="text" className="input" />
</div>
<div className="field">
<label className="label">Email</label>
<input {...register('email')} type="text" className="input" />
</div>
<div className="field">
<label className="label">Password</label>
<input {...register('password')} type="password" className="input" />
</div>
<div>
<button type="submit" className="button">
Register
</button>
</div>
</form>
);
};
export default Register;
As you can see we added register
function with a unique string - username, email and password to register every field. Most importantly is the result must be spreaded because it returns multiple props back.
Now after we submit our form in browser we get our onSubmit
console.log with all fields that we registered inside the form.
This is the simplest usage of React hook form which allows us inside onSubmit
to write any logic that we need to. For example we can make an API call to the backend with the data that we prepared.
Default values
But there is one more thing that I highly recommend you to do with this library. We can provide a default state of our form. You just saw that even without default state our form can work just fine. This is the case when all our default values are empty. But even in this case it makes a lot of sense to write down all fields so that we know what are default values there.
const {register, handleSubmit} = useForm({
defaultValues: {
username: '',
email: '',
password: ''
}
})
Even if you don't need to set default values when they are all empty if you implement an edit form of article for example you need to use this.
It also shows you directly what fields you are working with in your code.
Reusing components
The next important use case that you for sure have in your application is reusing some components of the form. You can see that in whole form we duplicate field
, label
and input
classes together. This is why it makes a lot of sense to move them to a separate component and reuse everywhere.
const Input = ({label, register}) => {
return (
<div className="field">
<label className="label">{label}</label>
<input {...register(label)} type="text" className="input"/>
</div>
)
}
We simply moves these 3 elements in the component. As we provide to it a label and register function from the library it will work just fine.
return (
<form className="form" onSubmit={handleSubmit(onSubmit)}>
<Input label='username' register={register}/>
...
</form>
)
As you can see in browser everything works in the same way even with moving our code to a separate component.
Client validation
The next important thing that you for sure need in your forms is client validation.
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("firstName", { required: true, maxLength: 20 })} />
<input {...register("lastName", { pattern: /^[A-Za-z]+$/i })} />
<input type="number" {...register("age", { min: 18, max: 99 })} />
<input type="submit" />
</form>
)
This is how it is done by default inside React hook forms library. You can provide a second parameter with different validations. It works but realistically it is difficult to read and using regular expression for validation is not the best idea for markup.
Lucking there is another way. We can write schema validation for our form. It is possible to create them with different libraries that React hook form supports like Yup
, Zod
, Superstruct
and Joi
. I want to show you the example with Yup
library but you can pick any of them.
npm i yup @hookform/resolvers
First one is validation library itself. The second one is the additional library of React hook forms with helps to work with different validation libraries.
import {yupResolver} from '@hookform/resolvers/yup'
const validationSchema = yup.object({
username: yup.string().required('Missing username'),
email: yup.string().required('Missing email').email('Invalid email format'),
password: yup.string().required('Missing password'),
}).required()
const Register = () => {
const {register, handleSubmit} = useForm({
resolver: yupResolver(validationSchema),
defaultValues: {
username: '',
email: '',
password: ''
}
})
}
Here we created a validation schema for our form. Every field is a key and we can apply different validations as a value. Most importantly for things like email
we don't need to write custom regular expression for validation because we get a built one from Yup
.
We also must provide this schema as a resolver for our useForm
hook.
Important point. If we submit a form now and it is invalid our onSubmit callback won't be called.
It happens because the form calls callback only with successful submission. There we have errors we must show them and not just to successful callback.
So now it is time to render errors when we have them.
const Register = () => {
const {register, handleSubmit, formState: {errors}} = useForm({
resolver: yupResolver(validationSchema),
defaultValues: {
username: '',
email: '',
password: ''
}
})
...
return (
<form className="form" onSubmit={handleSubmit(onSubmit)}>
<div className="field">
<label className="label">Username</label>
<input {...register('username')} type="text" className="input" />
{errors.username && (
<span className="error">{errors.username.message}</span>
)}
</div>
<div className="field">
<label className="label">Email</label>
<input {...register('email')} type="text" className="input" />
{errors.email && (
<span className="error">{errors.email.message}</span>
)}
</div>
<div className="field">
<label className="label">Password</label>
<input {...register('password')} type="password" className="input" />
{errors.password && (
<span className="error">{errors.password.message}</span>
)}
</div>
<div>
<button type="submit" className="button">
Register
</button>
</div>
</form>
);
};
We read validation errors from formsState.errors of React hook library and we use them to render error fields after every input. Our errors in an object with keys which are fields and values of error messages.
As you can see after hitting register button we got errors rendered on the screen. These are exactly client errors from our schema that were set by React hook form.
Backend validation
Now you know how to implement client error messages inside React hook form. But what about backend validation? Typically after submitting a form we make an API call and we might get error messages back. Like for example "Email is already taken". How can we render them with React hook form?
const Register = () => {
const {register, handleSubmit, formState: {errors}, setError} = useForm({
...
})
const onSubmit = data => {
console.log('data', data)
axios.post('https://api.realworld.io/api/users', {user: data}).then(response => {
console.log('succ', response)
}).catch(err => {
console.log('err', err)
if (err.response.data.errors.email) {
setError('email', {type: 'server', message: err.response.data.errors.email[0]})
}
if (err.response.data.errors.username) {
setError('username', {type: 'server', message: err.response.data.errors.username[0]})
}
if (err.response.data.errors.password) {
setError('password', {type: 'server', message: err.response.data.errors.password[0]})
}
})
}
}
Here we destructured additionally setError
from useForm
. It allows us to set errors manually. Next inside onSubmit
I'm doing a request to a real API to register a user. If it was not successful we real an error message from the err
property and with setError
we can set it to the specific field.
As you can see we successfully rendered backend errors when email and username are already taken.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!
📚 Source code of what we've done