Skip to content

Taming Forms With react-hook-form

Taming Forms With react-hook-form

Taming Forms With react-hook-form

After some time of doing repetitive tasks like handling forms in React.js, you will notice that there is a lot of boilerplate code that can be abstracted into reusable custom hooks. Luckily, there is plenty of existing Open Source solutions. In this case, we will be using react-hook-form.

What is react-hook-form

react-hook-form is a performant, flexible, and extensible form handling library built for React. It exports a custom hook that you can call within your Functional Components, and returns both a register function that you pass as a ref to your input components, and a handleSubmit function to wrap your submit callback.

By returning a register function that will be added to the input component, we can leverage the Uncontrolled Component pattern to make our application faster and more performant, by avoiding unnecessary re-renders.

What are we going to build?

To get a better understanding of what react-hook-form can do, we will build a simple application showing a list of characters and a form to add them to our list.

Screen-Recording-2020-10-19-at-1

Application Setup

Before going right into react-hook-form, we will need to prepare our application with the basic file structure and functionality. For this, we will create a new react application (you can either use your preferred starter or cloud IDE).

If you want to skip the application setup, you can go ahead and fork this CodeSandbox, but I highly recommend you at least read this section to have a better understanding of what the app does.

1. Characters List

Let's start by creating a new component where we will display our characters.

character-list.js

import React from "react";

function CharacterList({ characters }) {
  return (
    <div>
      <h2>Character List</h2>

      {characters.length === 0 ? (
        <p>
          <em>Your character list is empty</em>
        </p>
      ) : (
        <ul>
          {characters.map((character, id) => (
            <li key={id}>
              {character.name} (<strong>{character.species}</strong>)
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

export default CharacterList;

If you have a basic understanding of React, you will notice our CharacterList component will receive a characters prop, which is an array of objects with the properties name and species. If the array is empty, we will render a placeholder. Elsewhere, we will render the list.

2. Add Character Form

The second step is to create a component that will render the form to add a new character to our list.

character-form.js

import React from "react";

function CharacterForm({ addCharacter }) {
  const onSubmit = (data) => {
    addCharacter(data);
  };

  return (
    <div>
      <h2>Add character</h2>
      <form onSubmit={onSubmit}>
        <div>
          <input name="name" placeholder="Character name" />
        </div>
        <div>
          <select name="species">
            <option value="sorcerer">Sorcerer</option>
            <option value="knight">Knight</option>
            <option value="human">Human</option>
          </select>
        </div>
        <div>
          <button type="submit">Add character</button>
        </div>
      </form>
    </div>
  );
}

export default CharacterForm;

By itself, this component won't do anything because we are not doing anything with the collected data, nor validating our fields. This will be the component where we will be working on the next section of this tutorial.

3. The App

Now, let's just create the App component where we will render CharacterList and CharacterForm.

app.js

import React from "react";

import CharacterList from "./character-list";
import CharacterForm from "./character-form";

function App() {
  const [characters, setCharacters] = React.useState([]);

  const addCharacter = (character) => {
    setCharacters((characters) => [...characters, character]);
  };

  return (
    <div>
      <CharacterList characters={characters} />
      <hr />
      <CharacterForm addCharacter={addCharacter} />
    </div>
  );
}

export default App;

We will be saving our character list in characters by using the React.useState hook, and passing them down to CharacterList. Also, we created an addCharacter function that will just add a new character at the end of the characters list, and pass it to CharacterForm via prop.

Let's get to it!

Now that we have our application setup, let's see how can we leverage react-hook-form to take our forms to the next level.

Install react-hook-form

yarn add react-hook-form

Add react-hook-form to your CharacterForm

Here comes the fun. First, let's import useForm from react-hook-form, call the hook in our component, destructure register and handleSubmit methods out of it (don't worry, I will explain what they do just in a while), wrap our onSubmit function with handleSubmit, and pass register as the ref for each one of our form controls.

character-form.js

import React from "react";
+import { useForm } from "react-hook-form";

function CharacterForm({ addCharacter }) {
+  const { register, handleSubmit } = useForm();
+
-  const onSubmit = (data) => {
-    addCharacter(data);
-  };
+  const onSubmit = handleSubmit((data) => {
+    addCharacter(data);
+  });

  return (
    <div>
      <h2>Add character</h2>
      <form onSubmit={onSubmit}>
        <div>
-          <input name="name" placeholder="Character name" />
+          <input ref={register} name="name" placeholder="Character name" />
        </div>
        <div>
-          <select name="species">
+          <select ref={register} name="species">
            <option value="sorcerer">Sorcerer</option>
            <option value="knight">Knight</option>
            <option value="human">Human</option>
          </select>
        </div>
        <div>
          <button type="submit">Add character</button>
        </div>
      </form>
    </div>
  );
}

export default CharacterForm;

The register method

By attaching the register ref to our form controls, we can start tracking some stuff like the field value, its validation status, and even if the field had been touched or not.

Important: the name prop is required when passing the register ref, and it should be unique. This way, react-hook-form will know where to assign the field value. For more information, check out the register documentation.

The handleSubmit method

This is a function that wraps our submit callback, and passes the actual form values to it. Under the hood, it also calls preventDefault on the form event to avoid full page reloads. It can also be an asynchronous function.

For more information, check out the handleSubmit documentation.

Add some validations

At this point, we have a working form that is able to add characters to our list. However, we are not checking if the field is filled, in order to avoid empty submissions.

With react-hook-form, it is as simple as calling the register function with a configuration object defining the validation rules. For our case, we will make the name field required. Also, we can extract errors from useForm to show the user if the field has errors.

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

function CharacterForm({ addCharacter }) {
-  const { register, handleSubmit } = useForm();
+  const { register, handleSubmit, errors } = useForm();

  const onSubmit = handleSubmit((data) => {
    addCharacter(data);
  });

  return (
    <div>
      <h2>Add character</h2>
      <form onSubmit={onSubmit}>
        <div>
-          <input ref={register} name="name" placeholder="Character name" />
+          <input
+            ref={register({ required: true })}
+            name="name"
+            placeholder="Character name"
+          />
+          {errors.name && errors.name.type === "required"
+            ? "Name is required"
+            : null}
        </div>
        <div>
          <select ref={register} name="species">
            <option value="sorcerer">Sorcerer</option>
            <option value="knight">Knight</option>
            <option value="human">Human</option>
          </select>
        </div>
        <div>
          <button type="submit">Add character</button>
        </div>
      </form>
    </div>
  );
}

export default CharacterForm;

Reset the form status

The final step is to clear our form after successfully adding a character to our character list. For that, we will destructure a new method from the useForm hook: reset, and call it after addCharacter.

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

function CharacterForm({ addCharacter }) {
-  const { register, handleSubmit, errors } = useForm();
+  const { register, handleSubmit, errors, reset } = useForm();

  const onSubmit = handleSubmit((data) => {
    addCharacter(data);
+    reset();
  });

  console.log(errors.nameRequired);

  return (
    <div>
      <h2>Add character</h2>
      <form onSubmit={onSubmit}>
        <div>
          <input
            ref={register({ required: true })}
            name="name"
            placeholder="Character name"
          />
          {errors.name && errors.name.type === "required"
            ? "Name is required"
            : null}
        </div>
        <div>
          <select ref={register} name="species">
            <option value="sorcerer">Sorcerer</option>
            <option value="knight">Knight</option>
            <option value="human">Human</option>
          </select>
        </div>
        <div>
          <button type="submit">Add character</button>
        </div>
      </form>
    </div>
  );
}

export default CharacterForm;

For more information, check out the reset documentation.

Moving forward

Now that you have a better sense of how to manage your React forms, you have unlocked a new world of possibilities by using battle-tested and community-validated libraries like react-hook-form.

You can take a look at more advanced use cases, additional resources or even take a look at the full API.

If you want a finished code sample, you can check out this CodeSandbox.

This Dot is a consultancy dedicated to guiding companies through their modernization and digital transformation journeys. Specializing in replatforming, modernizing, and launching new initiatives, we stand out by taking true ownership of your engineering projects.

We love helping teams with projects that have missed their deadlines or helping keep your strategic digital initiatives on course. Check out our case studies and our clients that trust us with their engineering.