cookies

Hi! This website uses cookies. By continuing to browse or by clicking “I agree”, you accept this use. For more information, please see our Privacy Policy

bg

Building Effective Forms with React Hook Form, Typescript, Material UI, and Yup

author

Ihor Belehai

/

Front End Developer

9 min read

9 min read

Article content:

 

Front-end

Intro

Data is a vital tool for businesses of all kinds. And the more successful your data collection, the more valuable insights you’ll gain.

Creating effective and clear forms for your clients to fill out contributes to an efficient user experience and furthers your data collection and monetization goals.

Therefore, we’re starting a series of articles about building effective forms with React, Typescript, React Hook Form, Material UI, and Yup.

In this article, we’ll configure the necessary elements and determine the data structure. Then we’ll explain the React Hook Form library and its primary strengths.

If you want to see the final source code, you can check it out here.

You can also play with this demo.

What makes a form effective?

Let’s make a short checklist of what to keep in mind when creating a form to elicit and capture information.

The main takeaways are:

  1. Create a smooth process for filling out all the necessary fields;
  2. Ensure that it serves its intended purpose, in terms of recording, maintaining, and leveraging information;
  3. Make sure forms are complete and have a natural flow; and
  4. Keep the design visually appealing.

These are only the essential points to consider. Keeping them in mind creates efficient interaction between users and your business, facilitated by the Web.

See also: How to start with animations on Vue.js: a small guide for beginners

Getting started

To get started, we recommend you bootstrap this project quickly using our NextJS-TailwindCSS-Typescript template. You’re welcome to use and share it with others; hopefully, it helps you with your tasks.

Instead, you could always do a lengthy, boring manual setup, as follows:

1. Create React project:

npx create-react-app form-app

Or if you want to use NextJS:

npx create-next-app form-app

2. Add Typescript to the project.

3. Install dependencies:

npm i react-hook-form

Defining the data structure

Before starting to code, it’s always important to define the data structure of what you’re building. This ensures consistency and makes your code much more maintainable.

Let’s suppose your customer says they need an intake form for a veterinarian with the following wireframe:

A BIT OF REFACTORING WITH FORMPROVIDER AND USEFORMCONTEXT

According to the wireframe, the form will have the following structure:

title — required, min 2 characters, max 120

date — date picker, required, min date is today

selected plan — dropdown, one of Premium/Basic, required,

pet information:

name — required, min 2 characters, max 120

breed — required, min 2 characters, max 120

description — optional, min 2 characters, max 500

contact information:

first name — required, min 2 characters, max 120

last name — required, min 2 characters, max 120

phone number — required, correct phone number format

email — optional, correct email format

call me back — checkbox, true/false

Submit

Let’s start with the first bit of code.

Create the file models.ts in the root folder of your project and describe the form with Typescript interfaces.

 

export interface Appointment {
	id?: string;
  title: string;
  date: string;
  plan: AppointmentPlan
	pet: Pet;
  contact: ContactInfo;
}

export interface Pet {
  name: string;
  breed: string;
  description?: string;
}

export interface ContactInfo {
  firstName: string;
  lastName: string;
  phoneNumber: string;
  email?: string;
	callMeBack: boolean
}

export enum AppointmentPlan {
  Basic = 'Basic',
  Premium = 'Premium'
}

Create the form with useForm

1.Create a file: /components/AppointmentForm.tsx

2. Import some modules.

import { useForm, UseFormReturn, UseFormProps } from 'react-hook-form';
import { Appointment, AppointmentPlan } from '../models';

3. Create the object defaultValues: Appointment to store the initial state of the form.

const defaultValues: Appointment = {
  title: '',
  date: new Date().toString(),
	plan: AppointmentPlan.Basic,
  contact: {
    firstName: '',
    lastName: '',
    email: '',
    phoneNumber: '',
    callMeBack: false,
  },
  pet: {
    name: '',
    breed: ''
  },
};

4. Create a component called AppointmentForm and initialize the React Hook Form.

export const AppointmentForm = () => {
	// React Hook Form initialization
  const form: UseFormReturn<Appointment, UseFormProps> 
							= useForm<Appointment>({ defaultValues });

	// Submit handler
  const submitForm = () => {};

  return (
    <form onSubmit={submitForm}>
      <h1 className="font-semibold my-4 text-2xl text-center">
          Create a new appointment
      </h1>
      <button type="submit">Make an appointment!</button>
    </form>
  );
};

useForm returns the object of type UseFormReturn, which has a variety of actions that allow controlling the form:

  • register – registers input or selects validation rules in the form and applies them
  • formState – object containing information about the form state
  • watch – watches specified inputs and returns their values
  • handleSubmit – performs async validation and triggers the submit handler
  • reset – resets either the entire form state or part of the form state
  • resetField – resets an individual field state
  • setError – manually sets errors
  • clearErrors – that’s obvious!
  • setValue – sets value of registered field
  • setFocus – sets focus on input/selects by name
  • getValues – gets current form value by field name
  • getFieldState – individual field state
  • trigger – manually triggers form or input validation
  • control – object containing methods for registering components into React Hook Form

Let’s use some of them now and see what Hook Form is capable of.

5. Create form inputs for each field of the form and connect them to React Hook Form with the register method. As a parameter, specify a form property name corresponding to an input.

<form onSubmit={submitForm}>
  <h1>Create an appointment</h1>
  <input {...form.register('title')} type="text" />
	<input {...form.register('date')} type="date" />
	<select {...form.register('plan')} >
	  <option value={AppointmentPlan.Basic}>Basic</option>
	  <option value={AppointmentPlan.Premium}>Premium</option>
	</select>

	<h2>Pet information</h2>
	<input {...form.register('pet.name')} type="text" />
	<input {...form.register('pet.breed')} type="text" />
	<textarea
	  {...form.register('pet.description')}
	  type="text"
	/>
	
	<h2>Contact information</h2>
	<input {...form.register('contact.firstName')} type="text" />  
	<input {...form.register('contact.lastName')} type="text" />
	<input {...form.register('contact.phoneNumber')} type="text" />
	<input {...form.register('contact.email')} type="email" />
	<label htmlFor="contact.callMeBack">
	  Call me back to confirm order
	  <input
	    {...form.register('contact.callMeBack')}
	    type="checkbox"
	    id="contact.callMeBack"
	  />
	</label>
</form>

6.Wrap the submit handler with handleSubmit.

<form onSubmit={form.handleSubmit(submitForm)}>

handleSubmit is a function that performs a form validation and triggers the submit handler only when the validation runs successfully. This function is async, which means that React Hook Form performs asynchronous validation.

7. Now you can access the form value as an argument inside of your submit handler.

const submitForm = (appointment: Appointment) => {
	// todo: do whatever after submitting
};

8. Also, you may style the form and its layout to your taste; in my case, I used the TailwindCSS grid to set the layout. You can just copy-paste the code (we have more exciting topics to cover, so it’s best not to get distracted with these trifles).

import React from 'react';
import {
  useForm,
  UseFormProps,
  UseFormReturn,
  FormProvider,
} from 'react-hook-form';
import { Appointment, AppointmentPlan } from '../models';

const defaultValues: Appointment = {
  title: '',
  date: new Date().toString(),
  plan: AppointmentPlan.Basic,
  contact: {
    firstName: '',
    lastName: '',
    email: '',
    phoneNumber: '',
    callMeBack: false,
  },
  pet: {
    name: '',
    breed: '',
    description: '',
  },
};

export const AppointmentForm = () => {
  const form: UseFormReturn<Appointment, UseFormProps> = 
							useForm<Appointment>({ defaultValues });

  const submitForm = (form: Appointment) => {
		// todo: do whatever after submitting
  };

  return (
    <form onSubmit={form.handleSubmit(submitForm)}>
      <h1 className="font-semibold my-4 text-2xl text-center">
        Create a new appointment
      </h1>

      <section className="grid grid-cols-2 gap-6">
        <div className="col-span-2">
          <label htmlFor="title">Title*</label>
          <input {...form.register('title')} type="text" />
        </div>
        <div className="col-span-1">
          <label htmlFor="date">Date*</label>
          <input {...form.register('date')} type="date" id="date" />
        </div>
        <div className="col-span-1">
          <label htmlFor="plan">Select plan*</label>
          <select {...form.register('plan')} id="plan">
            <option value={AppointmentPlan.Basic}>Basic</option>
            <option value={AppointmentPlan.Premium}>Premium</option>
          </select>
        </div>
      </section>

      <h2 className="font-semibold my-4 text-lg">
	      Pet information
      </h2>
      <section className="grid grid-cols-2 gap-6">
        <div className="col-span-1">
          <label htmlFor="pet.name">Name*</label>
          <input {...form.register('pet.name')} type="text" id="pet.name" />
        </div>
        <div className="col-span-1">
          <label htmlFor="pet.breed">Breed*</label>
          <input {...form.register('pet.breed')} type="text" id="pet.breed" />
        </div>
        <div className="col-span-2">
          <label htmlFor="pet.description">Description</label>
          <textarea
            {...form.register('pet.description')}
            id="pet.description"
          />
        </div>
      </section>

      <h2 className="font-semibold my-4 text-lg">
	      Contact information
      </h2>
      <section className="grid grid-cols-2 gap-6">
        <div className="col-span-1">
          <label htmlFor="contact.firstName">First name*</label>
          <input
            {...form.register('contact.firstName')}
            type="text"
            id="contact.firstName"
          />
        </div>
        <div className="col-span-1">
          <label htmlFor="contact.lastName">Last name*</label>
          <input
            {...form.register('contact.lastName')}
            type="text"
            id="contact.lastName"
          />
        </div>
        <div className="col-span-1">
          <label htmlFor="contact.phoneNumber">Phone number*</label>
          <input
            {...form.register('contact.phoneNumber')}
            type="text"
            id="contact.phoneNumber"
          />
        </div>
        <div className="col-span-1">
          <label htmlFor="contact.email">Email</label>
          <input
            {...form.register('contact.email')}
            type="email"
            id="contact.email"
          />
        </div>
        <div className="col-span-2">
          <label htmlFor="contact.callMeBack">
            Call me back to confirm order
            <input
              {...form.register('contact.callMeBack')}
              type="checkbox"
              id="contact.callMeBack"
            />
          </label>
        </div>
      </section>

      <button type="submit">Make an appointment!</button>
    </form>
  );
};

export default AppointmentForm;

9. Run the app, fill in the form, and press the Make an appointment button. Take a look at the console and make sure that the form works.

See also: What is design thinking and why is it meant to be your guiding light?

A bit of refactoring with FormProvider and useFormContext

We’ve successfully set up the backbone of our future form and ensured that React Hook Form functions. But take a look at the code: this piece is too big. Let’s split it into several logical parts and discover some additional features of React Hook Form.

We’re going to create three new components, as shown in the illustration below:

AppointmentBaseForm
AppointmentPetForm
AppointmentContactForm

bg Building Effective Forms with React Hook Form, Typescript, Material UI, and Yup
1. Now, let’s go back to components/AppointmentForm.tsx
2. Here, we see a powerful feature of React Hook Form: useFormContext. Let’s import it:

import {
  useForm,
  UseFormProps,
  UseFormReturn,
	// importing FormProvider
  FormProvider,
} from 'react-hook-form';

3. Wrap your form with FormProvider by spreading the form as a prop:

<FormProvider {...form}>
  <form onSubmit={form.handleSubmit(submitForm)}>
    // ...
  </form>
</FormProvider>

Doing so lets all the nested components access the context of this form, which is an advantage because now, you don’t need to pass it as a prop. This allows you to quickly build as many nested forms as you need.

4. Next, you’ll create subcomponents of the form:

components/AppointmentBaseForm.tsx

5. Import useFormContext and some other modules, as depicted below.

import { useFormContext, UseFormProps, UseFormReturn } from 'react-hook-form';
import { Appointment, AppointmentPlan } from '../models';

6. Create the AppointmentBaseForm component, get it from context, and move the title, date, and plan fields there.

export const AppointmentBaseForm = () => {
	// getting form context
  const form: UseFormReturn<Appointment, UseFormProps> = useFormContext();

  return (
    <section className="grid grid-cols-2 gap-6">
      <div className="col-span-2">
        <label htmlFor="title">Title*</label>
        <input {...form.register('title')} type="text" />
      </div>
      <div className="col-span-1">
        <label htmlFor="date">Date*</label>
        <input {...form.register('date')} type="date" id="date" />
      </div>
      <div className="col-span-1">
        <label htmlFor="plan">Select plan*</label>
        <select {...form.register('plan')} id="plan">
          <option value={AppointmentPlan.Basic}>Basic</option>
          <option value={AppointmentPlan.Premium}>Premium</option>
        </select>
      </div>
    </section>
  );
};

In the example above, we get the object form by calling useFormContext. This is an object with the type UseFormReturn<Appointment and UseFormProps>, just like the object in the parent AppointmentForm component. Therefore, we can now use a register to work with our form inside this component.

7. Do the same with the rest of the components. Create AppointmentPetForm.tsx.

import React from 'react';
import { useFormContext, UseFormProps, UseFormReturn } from 'react-hook-form';
import { Appointment } from '../models';

export const AppointmentPetForm = () => {
  const form: UseFormReturn<Appointment, UseFormProps> = useFormContext();

  return (
    <section>
      <h2 className="font-semibold my-4 text-lg">
	      Pet information
      </h2>

      <div className="grid grid-cols-2 gap-6">
        <div className="col-span-1">
          <label htmlFor="pet.name">Name*</label>
          <input {...form.register('pet.name')} type="text" id="pet.name" />
        </div>
        <div className="col-span-1">
          <label htmlFor="pet.breed">Breed*</label>
          <input {...form.register('pet.breed')} type="text" id="pet.breed" />
        </div>
        <div className="col-span-2">
          <label htmlFor="pet.description">Description</label>
          <input
            {...form.register('pet.description')}
            type="text"
            id="pet.description"
          />
        </div>
      </div>
    </section>
  );
};

8. And create AppointmentContactForm.tsx.

import React from 'react'
import { useFormContext, UseFormProps, UseFormReturn } from 'react-hook-form';
import { Appointment } from '../models;

export const AppointmentContactForm = () => {
  const form: UseFormReturn<Appointment, UseFormProps> = useFormContext();

  return (
    <section>
      <h2 className="font-semibold my-4 text-lg">
	      Contact information
      </h2>

      <div className="grid grid-cols-2 gap-6">
        <div className="col-span-1">
          <label htmlFor="contact.firstName">First name*</label>
          <input
            {...form.register('contact.firstName')}
            type="text"
            id="contact.firstName"
          />
        </div>
        <div className="col-span-1">
          <label htmlFor="contact.lastName">Last name*</label>
          <input
            {...form.register('contact.lastName')}
            type="text"
            id="contact.lastName"
          />
        </div>

        <div className="col-span-1">
          <label htmlFor="contact.phoneNumber">Phone number*</label>
          <input
            {...form.register('contact.phoneNumber')}
            type="text"
            id="contact.phoneNumber"
          />
        </div>

        <div className="col-span-1">
          <label htmlFor="contact.email">Email</label>

          <input
            {...form.register('contact.email')}
            type="email"
            id="contact.email"
          />
        </div>
        <div className="col-span-2">
          <label htmlFor="contact.callMeBack">
            Call me back to confirm order
            <input
              {...form.register('contact.callMeBack')}
              type="checkbox"
              id="contact.callMeBack"
            />
          </label>
        </div>
      </div>
    </section>
  );
};

9. Import these newly-created components into AppointmentForm.tsx and apply them. In the end, our form looks clearer and more readable.

import React from 'react'
import {
  useForm,
  UseFormProps,
  UseFormReturn,
  FormProvider,
} from 'react-hook-form';
import { Appointment, AppointmentPlan } from '../models';
import { AppointmentBaseForm } from './AppointmentBaseForm';
import { AppointmentPetForm } from './AppointmentPetForm';
import { AppointmentContactForm } from './AppointmentContactForm';

const defaultValues: Appointment = {
  title: '',
  date: new Date().toString(),
  plan: AppointmentPlan.Basic,
  contact: {
    firstName: '',
    lastName: '',
    email: '',
    phoneNumber: '',
    callMeBack: false,
  },
  pet: {
    name: '',
    breed: '',
    description: '',
  },
};

export const AppointmentForm = () => {
  const form: UseFormReturn<Appointment, UseFormProps> = useForm<Appointment>({
    defaultValues,
  });

  const submitForm = (form: Appointment) => {
		// todo: do whatever after submitting
  };

  return (
    <FormProvider {...form}>
      <form onSubmit={form.handleSubmit(submitForm)}>
			   <h1 className="font-semibold my-4 text-2xl text-center">
	        Create a new appointment
			   </h1>
        <AppointmentBaseForm />
        <AppointmentPetForm />
        <AppointmentContactForm />
        <button type="submit">Make an appointment!</button>
      </form>
    </FormProvider>
  );
};

Debug the form with React Hook Form DevTools

Let’s make sure that the form works. I’ll show you another impressive tool from React Hook Form: Devtools. Yes, DevTools for managing forms makes debugging very easy. Let’s see how it works.

1.Install dependency:

npm i @hookform/devtools

2. Import it inside AppointmentForm.tsx.

import { DevTool } from "@hookform/devtools";

3. Apply it.

<>
	<FormProvider {...form}>
		// ...
	<FormProvider />
	<DevTool control={form.control} />
<>

4. If you go back to your browser, you should see that the following panel has appeared on the right side of the screen:

react hook form 21

Press [+] EXPAND to see detailed information about each of your fields. Try to input something into the form to see how it changes and see the magic work!

react hook form 22

We’ll go back to this tool in our next post to see how it works for other tasks.

motionlayout collapsing toolbar

Conclusion

In this post, you dipped your toe into form creation. We used examples to show you how to develop a basic appointment form using React Hook Form.

You saw how valuable React Hook is, as went through the entire process of developing a form from the beginning, including defining the data structure, creating the form, refactoring, and debugging. I hope you enjoyed the journey.

In the following article, I’ll implement form validation with Yup.