How I use React Hook Form with Yup and TypeScript
Tags: react, reacthookform, yup
Published: 2024-05-24 09:33:36

React Hook Form is a powerful library for managing forms in React applications. When combined with Yup for schema validation and TypeScript for type safety, it provides a robust solution for building and validating forms. This article will guide you through the process of integrating React Hook Form with Yup and TypeScript.

## Install the package into your project

To get started, create a new React project and install the necessary dependencies.

```
npm install react-hook-form yup @hookform/resolvers
```

## Example

```tsx
import Button from "@atoms/Button";
import Container from "@atoms/Container";
import InputField from "@atoms/InputField";
import TextAreaField from "@atoms/TextAreaField";
import Typography from "@atoms/Typography";
import { submitFormService } from "@features/contact/services/submitForm.service";
import { yupResolver } from "@hookform/resolvers/yup";
import { emailRules, phoneNumberRules } from "@utils/generalRegex.utils";
import handleError from "@utils/handleError.utils";
import { notifySuccess } from "@utils/notify.util";
import { useState } from "react";
import { useForm } from "react-hook-form";
import * as yup from "yup";
import s from "./ContactForm.module.scss";

interface IInitialValue {
  email: string;
  firstName: string;
  lastName: string;
  phoneNumber?: string;
  question?: string;
}
const initialValue: IInitialValue = {
  email: "",
  firstName: "",
  lastName: "",
  phoneNumber: "",
  question: "",
};
const ContactForm = () => {
  const resolver = yup.object({
    email: yup
      .string()
      .matches(emailRules, {
        message: "E-mail moet een geldig e-mailadres zijn!",
      })
      .required("Vul alstublieft dit veld in!"),
    firstName: yup.string().required("Vul alstublieft dit veld in!"),
    lastName: yup.string().required("Vul alstublieft dit veld in"),
    phoneNumber: yup
      .string()
      .test(
        "matches-phone",
        "Het mobiele telefoonnummer moet een geldig mobiel nummer zijn!",
        (value) => {
          if (!value) return true;
          return phoneNumberRules.test(value);
        }
      ),
    question: yup.string(),
  });

  const form = useForm({
    defaultValues: initialValue,
    resolver: yupResolver(resolver),
  });

  const onSubmit = async (data: typeof initialValue) => {
    try {
      await submitFormService({
        data: data,
      });
      form.reset();
      notifySuccess("Het contact is succesvol verzonden!");
    } catch (error) {
      handleError(error);
    }
  };

  return (
    <section className={s._Root}>
      <Container className={s._Container}>
        <form
          className={s._Card}
          onSubmit={form.handleSubmit(onSubmit)}
          method="post"
        >
          <Typography component="h2" variant="h2">
            Neem contact met ons op
          </Typography>
          <div className={s._Card__Row}>
            <InputField
              {...form.register("firstName")}
              id="voornaam"
              label="Voornam"
              helperText={form.formState.errors.firstName?.message}
              error={Boolean(form.formState.errors.firstName?.message)}
              required
            />
            <InputField
              {...form.register("lastName")}
              id="achternaam"
              label="Achternaam"
              helperText={form.formState.errors.lastName?.message}
              error={Boolean(form.formState.errors.lastName?.message)}
              required
            />
          </div>
          <div className={s._Card__Row}>
            <InputField
              {...form.register("phoneNumber")}
              id="telefoonnummer"
              label="Telefoonnummer"
              helperText={form.formState.errors.phoneNumber?.message}
              error={Boolean(form.formState.errors.phoneNumber?.message)}
            />
            <InputField
              {...form.register("email")}
              id="e-mail"
              label="E-mail"
              helperText={form.formState.errors.email?.message}
              error={Boolean(form.formState.errors.email?.message)}
              required
            />
          </div>
          <div className={s._Card__Row}>
            <TextAreaField
              id="jouwVraag"
              label="Jouw vraag"
              {...form.register("question")}
            />
          </div>
          <Button
            component="button"
            type="submit"
            disabled={form.formState.isSubmitting}
          >
            {form.formState.isSubmitting ? "Bezig met laden..." : "Verzend"}
          </Button>
        </form>
      </Container>
    </section>
  );
};

export default ContactForm;
```

## Let's breakdown part by part

### 1. setup the initial value with the type or interface

define the default value of the form that you will create

```tsx
...
interface IInitialValue {
  email: string;
  firstName: string;
  lastName: string;
  phoneNumber?: string;
  question?: string;
}
const initialValue: IInitialValue = {
  email: "",
  firstName: "",
  lastName: "",
  phoneNumber: "",
  question: "",
};
...
```

### 2. setup the handle form with useForm

```tsx
const form = useForm({
  defaultValues: initialValue,
  resolver: yupResolver(resolver),
});
```

### 3. write the resolver or validation

```tsx
...
const resolver = yup.object({
  email: yup
    .string()
    .matches(emailRules, {
      message: "E-mail moet een geldig e-mailadres zijn!",
    })
    .required("Vul alstublieft dit veld in!"),
  firstName: yup.string().required("Vul alstublieft dit veld in!"),
  lastName: yup.string().required("Vul alstublieft dit veld in"),
  phoneNumber: yup
    .string()
    .test(
      "matches-phone",
      "Het mobiele telefoonnummer moet een geldig mobiel nummer zijn!",
      (value) => {
        if (!value) return true;
        return phoneNumberRules.test(value);
      }
    ),
  question: yup.string(),
});
...
```

- email field use the custom rules for email, in this case I use the regex from `emailRules`
- firstName & lastName field is required
- question is not required and only allow string value
- phoneNumber is not required but the value should the correct phoneNumber in this case I use regex to handle the rules

### 4. register each field to useForm

I register each field like below. I not only register, I also put the error state and error message when the value not match with the rules. and make the field disabled when the form submiting

```tsx
...
<InputField
    {...form.register("firstName")}
    id="voornaam"
    label="Voornam"
    helperText={form.formState.errors.firstName?.message}
    error={Boolean(form.formState.errors.firstName?.message)}
    disabled={form.formState.isSubmitting}
    required
/>
...
```

### 5. Handle submit

```tsx
...
const onSubmit = async (data: typeof initialValue) => {
  try {
    await submitFormService({
      data: data,
    });
    form.reset();
    notifySuccess("Het contact is succesvol verzonden!");
  } catch (error) {
    handleError(error);
  }
};
...
<form className={s._Card} onSubmit={form.handleSubmit(onSubmit)} method="post">
  ...
</form>;
...
```

### 6. Add state to Button

Add state text if necessary, and make the button disabled while submitting

```tsx
...
<Button component="button" type="submit" disabled={form.formState.isSubmitting}>
  {form.formState.isSubmitting ? "Bezig met laden..." : "Verzend"}
</Button>
...
```

## Conclusion

combining React Hook Form with Yup and TypeScript, you can create powerful, type-safe forms with robust validation. This integration allows you to leverage the strengths of each library: React Hook Form for efficient form management, Yup for schema-based validation, and TypeScript for static type checking.

This setup ensures that your forms are not only functional but also maintainable and easy to debug, providing a solid foundation for building complex forms in your React applications.