DEV Community

Cover image for Taming React forms with validations and masks using IMask, Yup, and React Hook Form.
Pedro Arantes for ttoss

Posted on

Taming React forms with validations and masks using IMask, Yup, and React Hook Form.

TL;DR

The final code can be seen at this codesandbox. The implementation is explained here.

Introduction

Creating forms with React was always a pain for me. I'm sure this happened because of some lack of technical knowledge. It could about HTML inputs and the data flow inside de input tag, how to handle validations and masks properly. But the biggest problem was that I had an unclear roles attribution in the component. By roles, I mean:

  • Which part will handle the user input?
  • Which methods will handle validations?
  • If we need masks, should we keep them before calling the post method or validations?
  • When do I have to apply masks? And unmask?

Imagine a React component with validations, masks, form methods, API callings all together in a messy code. That was the way I used to create React forms. Because of this, I decided to study more about forms.

The biggest issue with forms was the lack of clarity of the roles of each form part. The focus of my study was trying to figure out the existing roles and how to decouple them. Once these attributions were clear, a combination with IMask, Yup, and React Hook Form was used to take care of these roles.

In this article, we won't talk about the implementation details of the libraries used. The main goal is to present an example using them together. Before showing the code, let's define the roles I'm talking about.

Form Roles

A form is a tool that is used to gather information in a structured and convenient way. Thus, we have the first role:

1. User interaction. Input and output. UI/UX.

To achieve a good UX, sometimes we have to manipulate the way we display data to users. For instance, adding punctuations and spaces when a user types their phone number. The displayed data may be different from the data we'll send to the form processor - the entity that will receive the final data, like a POST endpoint. This technique is called data masking and is our second role:

2. Data masking.

Before data is being sent to the processor, some data must be validated. This is very correlated with user experience. When the user types some information, if we detect that it is invalid, we should display some notification as soon as possible. That is our third role:

3. Data validation.

Finally, our last role is about handling forms actions, states, and events. As the user interacts with the form, we sometimes need to know when the user starts typing, when leaving an input, if the form contains some error, if it is submitted or submitting...

4. Form handler.

In summary, our roles are:

The Combination

We're going to create a form with the fields: email, date, CPF or CNPJ, phone number, and currency. Each one of them has its validations and masks.

A form with this implementation was created.

User Interaction

Implementation can be seen here.

HTML and CSS (coded in the React way) are the ones that take care of this part. Some questions that drive this role are:

  • How can we provide an easy and intuitive interface?
  • How is the best way to present data to users?
  • How to create a seamless way to user input data?

Data Masking

Implementation can be seen here.

This role takes care of data manipulation, in our case, masking. A helper that has some manipulation methods was created, I called it masker. It uses IMask under the hood to perform masking and unmasking.

/**
 * https://stackoverflow.com/a/10452789/8786986
 * @param args
 */
const masker = ({
  masked,
  transform,
  maskDefault
}: {
  masked: any;
  transform?: any;
  maskDefault?: any;
}) =>
  (function () {
    const mask = IMask.createPipe(
      masked,
      IMask.PIPE_TYPE.UNMASKED,
      IMask.PIPE_TYPE.MASKED
    );

    const unmask = IMask.createPipe(
      masked,
      IMask.PIPE_TYPE.MASKED,
      IMask.PIPE_TYPE.UNMASKED
    );

    const onChange = (e: any) => {
      const unmasked = unmask(e.target.value);
      const newValue = mask(unmasked);
      e.target.value = newValue;
    };

    return {
      mask,
      onChange,
      transform: transform || unmask,
      unmask,
      maskDefault: maskDefault || mask
    };
  })();
Enter fullscreen mode Exit fullscreen mode

The first exposed method is the onChange. We use it together with <input /> to handle the HTML input onChange event. This method takes the data typed by the user, applies the mask, and sets the masked back to <input />.

<Input
  id="cpfOrCnpj"
  name="cpfOrCnpj"
  onChange={masks.cpfOrCnpjMask.onChange}
/>
Enter fullscreen mode Exit fullscreen mode

The second method is the transform and it's used with validations. This method takes the masked value and transforms it into the format that we'll send to the form processor.

The third one is maskDefault. This method takes initial values sent by the form processor and masks them. Once transformed, the user we'll see the initial data with masking. It's used with form handler.

Data Validation

Implementation can be seen here.

Yup is the one that manages this role. We create a schema that performs all data validations at the validation phase.

export const schema = yup.object().shape({
  email: yup.string().email().required(),
  date: yup
    .string()
    .transform(masks.dateMask.transform)
    .notRequired()
    .test("validateDate", "Invalid date", (value) => {
      return dateFns.isValid(dateFns.parse(value, "yyyy-MM-dd", new Date()));
    }),
  ...
}
Enter fullscreen mode Exit fullscreen mode

Note how masker.transform was used. When validations are triggered, the data enters the Yup pipe with the mask. Before the validations start, we transform from masked to form processor format. For instance, if a phone number enters equals to +55 16 91234-1234, it's transformed to 16912341234.

Form Handler

Implementation can be seen here.

The chosen form handler was React Hook Form. It connects with the <input /> by the register method.

It uses masker.maskDefault property to apply the mask to initial values that will be passed to React Hook Form useForm defaultValues property. getData in the code below represents the method that returns the initial value from form processor.

const getDefaultValues = () => {
  const data = getData();
  return {
    ...data,
    date: masks.dateMask.maskDefault(data.date),
    cpfOrCnpj: masks.cpfOrCnpjMask.maskDefault(data.cpfOrCnpj),
    phone: masks.phoneMask.maskDefault(data.phone),
    currency: masks.currencyMask.maskDefault(data.currency)
  };
};
Enter fullscreen mode Exit fullscreen mode

Finally, the last step of this role is the submit. When the user submits and all data is valid, it handles the submission. postData represents the method that will send the data to the form processor. Also, it's important to remember that the data from handleSubmit has the form processor format because the transform called at the beginning of the Yup schema.

const { handleSubmit, reset } = form;
const onSubmit = handleSubmit((data) => postData(data));
Enter fullscreen mode Exit fullscreen mode

Conclusion

In my opinion, these libraries created a great fit together. This combination wasn't widely tested for me, but I'm almost sure that it'll be suitable for many form implementations.

I'm open to discussions and I want to hear your feedback. You can comment here or reach me on Twitter, my DMs are open.

I hope you enjoy this text as I did writing it. I really hope this article helps you 😁


Photo by Cytonn Photography on Unsplash

Top comments (0)