Using useFieldArray with React Hook Form and TypeScript

After you have a react form built or when starting a new one from scratch, you may need a specific type of data that the user may have 1 or more. One quick example is when a person has multiple phone numbers. Your project may require to capture this data in a structured way without limiting the user to just enter one, or worse to enter this information in a single field that would require further processing from our end to split this data. To cover this specific use case we use field arrays.

Field Arrays help us give structure to a piece of data that may have 1 or more entries, similar to how you store data in an array. Field Arrays can be tricky to create and validate from scratch, but on this post we’ll implement a field array to an existing react hook form using TypeScript. In case you have not seen my previous post about creating a react hook form, check it out and come back to this post. Or if you have your own react hook form ready you can follow along.

Importing useFieldArray with React Hook Form

As always, we have to install the relevant libraries, and in this case it is react hook form. You should have it installed already or if not install it now. For those who don’t have it installed just run (inside your project root folder):
npm install react-hook-form --save

In case you don’t have an existing react hook form implemented already, this post gives you an intro to creating a react hook form from scratch. Great, now that you have that installed, let’s continue importing the relevant react hook that will help use field arrays: useFieldArray.

At the top of App.tsx import useFieldArray:

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

Next we’ll call our useFieldArray hook with the control property to connect our form to our field array, then we’ll give it a name. In this case the name will be phoneNumbers. We’ll also add a basic array validator to our phoneNumbers field array.

type IFormInput = {
  firstName: string;
  lastName: string;
  email: string;
  phoneNumbers?: {
    phone: string;
  }[];
  subscribe: SubscriptionEnum;
};

const errMsg = {
  firstNameErr: "Invalid First Name. Must be at least 2 characters long",
  lastNameErr: "Invalid Last Name. Must be at least 2 characters long",
  emailErr: "Invalid email. Must be a valid name@example.com",
  subscribe: 'You must select "yes" or "no"',
};

const schema: yup.ObjectSchema<IFormInput> = yup.object({
  firstName: yup.string().min(2, errMsg.firstNameErr).required(),
  lastName: yup.string().min(2, errMsg.lastNameErr).required(),
  email: yup.string().email(errMsg.emailErr).required(errMsg.emailErr),
  phoneNumbers: yup.array(
    yup.object({
      phone: yup.string().validPhone("Bad Phone Number").required(),
    })
  ),
  subscribe: yup.string<SubscriptionEnum>().defined(errMsg.subscribe),
});

function App() {
  const {
    register,
    handleSubmit,
    control,
    formState: { errors },
  } = useForm<IFormInput>({
    resolver: yupResolver(schema),
    defaultValues: {
      phoneNumbers: [],
    },
  });

  const { fields, append, remove } = useFieldArray({
    control,
    name: "phoneNumbers",
  });
 // more code below
}

We first added our phoneNumbers field to the IFormInput type which tells TypeScript what fields our form has and what is the type for each one. We are telling it that phoneNumbers is an array of objects. We added a “?” mark to tell TypeScript this field is optional. Next, we added an array validator to our field array for now.

Below we exported the control property to connect our form with our field array. We used this as a parameter to our useFieldArray call. We then finally gave a name to our field array “phoneNumbers”.

We connected the internal library API but we haven’t yet displayed our field array, and we’ll do that next.

Adding the Field Array field

To add the actual field array, we need to have an idea of how we want to display this in the form. Usually we show an “Add” button then when clicked we insert a field array item, and inside each field array item we have a button in case our user wants to delete a field array item.

The good news is that react hook form give us a head start to help us implement this functionality quickly into our react hook form using the react hook form library functions: append, update and remove which are ready to use callbacks from react hook form.

These callbacks do exactly as their names suggest: adding a new item to the field array, updating an item of a field array and removing an item from the field array.

Let’s modify our react form inserting our field array and placing the callbacks in the precise locations where we need them.

  return (
    <div className="App">
      <form onSubmit={handleSubmit(onFormSubmit)}>
        <div className={`field ${hasError.firstName ? "has-error" : ""}`}>
          <label>First Name</label>
          <input {...register("firstName")} />
          <span>{errors.firstName?.message}</span>
        </div>
        <div className={`field ${hasError.lastName ? "has-error" : ""}`}>
          <label>Last Name</label>
          <input {...register("lastName")} />
          <span>{errors.lastName?.message}</span>
        </div>
        <div className={`field ${hasError.email ? "has-error" : ""}`}>
          <label>Email</label>
          <input {...register("email")} />
          <span>{errors.email?.message}</span>
        </div>
        <div className={`field ${hasError.subscribe ? "has-error" : ""}`}>
          <label>Subscribe</label>
          <select {...register("subscribe")}>
            <option value="yes">Yes</option>
            <option value="no">No</option>
          </select>
          <span>{errors.subscribe?.message}</span>
        </div>

        <button type="button" onClick={() => append({ phone: "" })}>
          Add Phone
        </button>

        <div className="field">
          {fields.map((field, index) => {
            return (
              <div key={field.id}>
                <section className="phoneNumber">
                  <input
                    placeholder="Your Phone Number"
                    type="phone"
                    {...register(`phoneNumbers.${index}.phone` as const)}
                  />
                </section>
                <span>{errors.phoneNumbers?.[index]?.phone?.message}</span>
                <button onClick={() => remove(index)}>Remove</button>
              </div>
            );
          })}
        </div>

        <div className="submit-btn">
          <input type="submit" />
        </div>
      </form>
    </div>
  );

Awesome work. We now have our field array in place along with the add button and a button to delete each field array item. Notice how we call remove(index) to remove the current field array item. Also see how we reveal the error: {errors.phoneNumbers?.[index]?.phone?.message} – finally see the Add Phone button how the onClick handler calls the append function with the data: {phone: “”} to insert a new phone number field array item.

Now let’s test our work:
npm start

We should be able to see our form with an “Add phone” button. Clicking this button should add a new phone number field. If we click the “Remove” button it should remove that phone number.

react hook form usefieldarray gives us a base to start working on field arrays, with a full field array logic for free. But we have to use our creativity to implement proper validation to our field array.

Validating Data with useFieldArray

Our data should be validated, we should never trust data coming from the user, also we have to help the user as much as we can to see any mistakes they have made when typing any data into the input fields.

We are going to continue using yup to validate our form data. Our field array is a phone number. There are many phone number formats depending on the country the phone number is from. To make our form scalable we have to think outside the box sometimes and ask: “What if I need to validate phone numbers from more countries later?” – just asking that question can make you be more vigilant on how you implement a solution.

In our case we are using yup. But those who know yup may be saying: “but yup doesn’t validate phone numbers” and yes, you are right. So we’ll have to extend yup to validate phone numbers. Another option is to use regular expression validation, but remember – if you are asked to validate phone numbers from multiple countries this can get really complex.

By looking at the documentation of yup we find that yup has an “addMethod” method just for this. As stated above validating phone numbers can be complicated if we are dealing with multiple countries. For this reason we are going to use another external library for phone number validation: libphonenumber-js – on your terminal (type inside your project root folder):

npm install libphonenumber-js --save

Let’s import it at the top and use the convenient function called isValidPhoneNumber:

import { isValidPhoneNumber } from "libphonenumber-js";

Now let’s add a method to yup to ensure that we can validate phone numbers using this library (you can add this at the top of Apps.tsx but below all the import statements):

yup.addMethod<yup.StringSchema>(yup.string, "validPhone", function (message) {
  return this.test("validPhone", message, (value) => {
    if (!value) {
      return false;
    }
    return isValidPhoneNumber(value, "US");
  });
});

We are using TypeScript generics next to the addMethod call, telling TypeScript that we are using the yup StringSchema type which makes our method be accessible after .string() calls and have access to yup string internal methods. Next we pass the type of our data that yup understands as a string (yup.string), the name of our method “validPhone” and finally a callback with a message parameter. Inside we return the result of the this.test() using the name of our custom method “validPhone” a message parameter, and another callback that receives the value of the field.

Inside the callback we say if there is no value, we consider the field invalid (by returning false) otherwise we validate our phone number returning the result of isValidPhoneNumber passing the value of the field, and the country code.

That was a lot on a short piece of code, but once you master this, you can now extend yup as you’d like.
Next, let’s add it to the validation schema we created on a previous post so we can integrate this new yup validator for our phone numbers:
const schema: yup.ObjectSchema<IFormInput> = yup.object({
  firstName: yup.string().min(2, errMsg.firstNameErr).required(),
  lastName: yup.string().min(2, errMsg.lastNameErr).required(),
  email: yup.string().email(errMsg.emailErr).required(errMsg.emailErr),
  phoneNumbers: yup.array(
    yup.object({
      phone: yup.string().validPhone("Bad Phone Number").required(),
    })
  ),
  subscribe: yup.string<SubscriptionEnum>().defined(errMsg.subscribe),
});

We are saying we have an array of objects with a phone property. Inside we call our custom yup validator validPhone passing our custom error message. Great, if you implemented this correctly, it should work for you. But before we proceed you may be seeing an error on your IDE (I use VS Code) with TypeScript not recognizing our validPhone() method call. What can we do if this is our own custom method and is not part of yup’s codebase nor method interface?

Things can get messy quickly, but fortunately TypeScript allows us to augment existing types so we don’t have to go to yup’s Github page asking for a custom method:

declare module "yup" {
  interface StringSchema {
    validPhone(message: string): StringSchema;
  }
}

We finally did it. Now TypeScript is happy with our new custom method. We are extending the “yup” module StringSchema interface to have our validPhone method that includes a message parameter as a string and returns a StringSchema type.

Let’s do a final test:
npm start
We can now enter data on our form and see how the validation is making our user experience much better. If we add a phone number field we must enter a valid phone. If we don’t add any phone numbers, only the other fields get validated. Awesome work!

Conclusion

We took a deep dive into form field arrays, validation and custom yup methods. react hook form useFieldArray is truly a great addition that makes these use cases possible when using react hook form. We then implemented a simple logic to add and remove phone numbers, as the heavy lifting is inside react hook form.

We also made it possible to implement everything in a TypeScript friendly way, so we can easily maintain our code and keep it as clean and as bug free as possible. We also learned how to augment TypeScript library types, in this case yup’s library to be able to recognize our custom validator method.

This was just a small taste of the immense possibilities that you have at your disposal to implement field arrays to make your forms more adaptable to your users. Now go create powerful and scalable forms for your own projects. Have comments or know anything we can do to improve it? comment below! – see you soon.

About The Author

Leave a Comment

Your email address will not be published. Required fields are marked *