React Hook Form Crash Course - Speed Up Writing React Form
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.

website

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.

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.

Empty response

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

example

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.

Errors

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.

Backend errors

As you can see we successfully rendered backend errors when email and username are already taken.

And actually if you want to improve your React knowledge and prepare for the interview I highly recommend you to check my course React Interview Questions.

📚 Source code of what we've done