Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

Master-detail forms with React Hook Form

May 19, 2020
typescriptreact

In the last couple of posts, we have built a form to capture a name, an email address, and a score with some reasonably complex validation rules.

In this post, we will capture multiple scores from a person and turn our form into a master-detail form. We will start with the form we implemented in the last post.

Using useFieldArray

We are going to use React Hook Form’s useFieldArray to help render and manage the scores. So, let’s import this:

import {
  useForm,
  useFieldArray} from "react-hook-form";

We are going to capture more than one score now, so, let’s change the field name to scores and change it to an array:

type PersonScore = {
  name: string;
  email: string;
  scores: number[];};

useFieldArray needs to use the control object from useForm, which is the controller for the form. So, let’s destructure this:

const {
  register,
  handleSubmit,
  errors,
  control} = useForm<PersonScore>();

We can now use useFieldArray:

const { fields, append, remove } = useFieldArray(
  {
    control,
    name: "scores"
  }
);

We have destructured the following from useFieldArray:

  • fields. This is an array that will help us render each score field
  • append. This is a function that will allow us to add score fields
  • remove. This is a function that will enable us to remove score fields

Ensuring there is at least one score

We want to start with a single score field. So, let’s use the append function to add a score to the array:

if (fields.length === 0) {
  append({});
}

The function takes in an object containing default values for the fields. We have no default values, so we have passed an empty object.

Rendering score fields

Let’s move on to render the score fields:

{
  fields.map((score, index) => (
    <div key={score.id}>
      <div className="field">
        <label htmlFor={`scores[${index}]`}>
          Score{` ${index + 1}`}
        </label>
        <input
          type="number"
          id={`scores[${index}]`}
          name={`scores[${index}]`}
          ref={register({
            required: true,
            min: 0,
            max: 100,
            validate: isEven
          })}
        />
      </div>
    </div>
  ));
}

We are mapping over the fields array that contains all the scores. We render a label and input for each score.

Notice that we use the id property from the items in the fields array as the React component key. This is a guid that useFieldArray gives us.

We need to name the fields score[0], score[1], score[2], … so that useFieldArray can manage them.

Our validation rules remain the same as the previous implementation of the form from the last post. Let’s render the validation error messages:

{
  errors.scores &&
    errors.scores[index] &&
    errors.scores[index].type === "required" && (
      <div className="error">
        Your must enter your score.
      </div>
    );
}
{
  errors.scores &&
    errors.scores[index] &&
    errors.scores[index].type === "min" && (
      <div className="error">
        Your score must be at least 0
      </div>
    );
}
{
  errors.scores &&
    errors.scores[index] &&
    errors.scores[index].type === "max" && (
      <div className="error">
        Your score must be no more than 100
      </div>
    );
}
{
  errors.scores &&
    errors.scores[index] &&
    errors.scores[index].type === "validate" && (
      <div className="error">
        Your score must be and even number
      </div>
    );
}

Notice that the scores errors are now in an array. Apart from that, the structure of each array is the same as a regular field.

Buttons to add and remove score fields

Let’s add a button to add a score field:

<button
  className="add"
  onClick={(e: React.MouseEvent) => {
    e.preventDefault();
    append({});
  }}
>
  Add score
</button>

When the button is clicked, we call the append function from useFieldArray to add a new score.

Notice that we have called the events preventDefault method to ensure the form isn’t submitted when the button is pressed.

Let’s also add a button to remove a score field:

<button
  className="remove"
  onClick={(e: React.MouseEvent) => {
    e.preventDefault();
    remove(index);
  }}
>
  Remove
</button>

When the button is clicked, we call the remove function from useFieldArray passing in the index of the score to remove.

That’s it. A working version of this form is available on CodeSandbox

Wrap up

We’ve reached the end of this series of posts on React Hook Form, where we have used it to implement some complex but common form requirements. React Hook Form is a great form library that is simple to use and also very performant - well worth giving it a try when implementing your next form!

Did you find this post useful?

Let me know by sharing it on Twitter.
Click here to share this post on Twitter

If you to learn more about using TypeScript with React, you may find my course useful:

Using TypeScript with React

Find out more