DEV Community

Ammar Tinwala
Ammar Tinwala

Posted on

Forms in React: React Hook Forms with Material UI and YUP

In React there are lots of ways to write a form, some use libraries like Formik, Redux Form or some prefer the plain way of doing it writing everything from scratch. The advantage of using a form library is a lot of common form functionality is taken care of like validations, getting the whole form data in a single object and writing less code(this point is debatable :D)
One such form library in react is React Hook Form

Why I choose to use React Hook Form?

I have tried a couple of form libraries the most popular being Formik, but none of these is, as fast as React Hook Form. In my web apps, my form usually has around 60-70 fields, and for such a large amount of fields, no form library comes close to React Hook Form in terms of performance, not even Formik.

GOAL

In this article, we will cover how to create reusable form components like TextField, Select of Material UI, MultiSelect of react-select with React Hook Form. We will be using Yup for form validations and how it integrates with React Hook Form.

At the end of the article, I will share a git hub repo where I have included all the form components of Material UI with React Hook Form that one can easily reference or integrate into their projects

Table of Content

This article will be a long one. So I have divided my article into few sections

Initial setup

We will use create-react-app for this article. Follow the below steps to setup the basics

npx create-react-app hook-form-mui
cd hook-form-mui
npm install @material-ui/core @material-ui/icons react-hook-form yup @hookform/resolvers react-select styled-components @material-ui/pickers @date-io/moment@1.x moment
Enter fullscreen mode Exit fullscreen mode

Once all the packages are installed, run the app once.

npm start
Enter fullscreen mode Exit fullscreen mode

You will see the below page
Alt Text

Basic form element binding with React Hook Form

1. Textfield

Create a folder in the src named controls. Inside controls folder create a folder input. Inside input folder create a file index.js (src -> controls -> input -> index.js)

index.js will have below code

import React from "react";
import { useFormContext, Controller } from "react-hook-form";
import TextField from "@material-ui/core/TextField";

function FormInput(props) {
  const { control } = useFormContext();
  const { name, label } = props;


  return (
    <Controller
      as={TextField}
      name={name}
      control={control}
      defaultValue=""
      label={label}
      fullWidth={true}
      {...props}
    />
  );
}

export default FormInput;

Enter fullscreen mode Exit fullscreen mode

Lets deep dive into the above code.
When using React Hook Form, two primary concepts need to be kept in mind,

  1. We have to register each form field that we use. This helps in form submission and validation.
  2. Each form field should have a unique name associated with it.

In the above code, we are using a wrapper component called Controller provided by react-hook-form to register our form fields (in this case) TextField component.

As you can see we can pass additional props of TextField component and other props directly to the Controller component

 <Controller
      as={TextField}
      name={name}
      control={control}
      defaultValue=""
      label={label}
      fullWidth={true}
      InputLabelProps={{
        className: required ? "required-label" : "",
        required: required || false,
      }}
      error={isError}
      helperText={errorMessage}
      {...props}
    />
Enter fullscreen mode Exit fullscreen mode

control object contains methods for registering a controlled component into React Hook Form. The control object needs to be pass as a prop to the Controller component.
control object is declared as :

const { control } = useFormContext();
Enter fullscreen mode Exit fullscreen mode

In App.js, we will have the following code:

import React from "react";
import { useForm, FormProvider } from "react-hook-form";
import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";

import FormInput from "./controls/input";

function App(props) {
  const methods = useForm();
  const { handleSubmit } = methods;

  const onSubmit = (data) => {
    console.log(data);
  };

  return (
    <div style={{ padding: "10px" }}>
      <Button
        variant="contained"
        color="primary"
        onClick={handleSubmit(onSubmit)}
      >
        SUBMIT
      </Button>

      <div style={{ padding: "10px" }}>
        <FormProvider {...methods}> // pass all methods into the context
          <form>
            <Grid container spacing={2}>
              <Grid item xs={6}>
                <FormInput name="name" label="Name" />
              </Grid>
            </Grid>
          </form>
        </FormProvider>
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Let's deep dive into the App.js code.
The most important function is useForm() which is a hook provided by react-hook-form. useForm() contains various methods which is required for form validation, submission and registration of the form fields.

 const methods = useForm();
 const { handleSubmit } = methods;
Enter fullscreen mode Exit fullscreen mode

As in the above code useForm() provides a method object which contains handleSubmit function which is used for form submission on button click. In this case SUBMIT button.

 <FormProvider {...methods}> 
          <form>
            <Grid container spacing={2}>
              <Grid item xs={6}>
                <FormInput name="name" label="Name" />
              </Grid>
            </Grid>
          </form>
        </FormProvider>
Enter fullscreen mode Exit fullscreen mode

In the above block of code, we declare a FormProvider component under which our form and its respective fields will be declared. Also, we need to pass all the functions and objects of methods to FormProvider component. This is required as we are using a deep nested structured of custom form fields and the useFormContext() used in the FormInput component need to consume the functions and objects of methods

For the FormInput component we just need to pass name and label props.

<FormInput name="name" label="Name" />
Enter fullscreen mode Exit fullscreen mode

If you run the app, you should see:
Alt Text

Type any text in the Name field and click on SUBMIT button. In the dev-console check the output. The output will be an object with the field name and corresponding value.

Now let's move on to create other field components in a similar fashion.

2. Select

Create a new folder name styles under src. Create a new file index.js under styles folder (src -> styles -> index.js)
index.js will have following code

import styled from "styled-components";
import { InputLabel } from "@material-ui/core";

export const StyledInputLabel = styled(InputLabel)`
  && {
    .req-label {
      color: #f44336;
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

I'm using styled-components for my styling. StyledInputLabel will be used below in the FormSelect component. The main purpose of the above styling will be used during validation.

Create a new folder name select under controls, inside select folder create a index.js file (controls -> select -> index.js).

index.js will have following code

import React from "react";
import { useFormContext, Controller } from "react-hook-form";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select";
import InputLabel from "@material-ui/core/InputLabel";

const MuiSelect = (props) => {
  const { label, name, options } = props;

  return (
    <FormControl fullWidth={true}>
      <InputLabel htmlFor={name}>{label}</InputLabel>
      <Select id={name} {...props}>
        <MenuItem value="">
          <em>None</em>
        </MenuItem>
        {options.map((item) => (
          <MenuItem key={item.id} value={item.id}>
            {item.label}
          </MenuItem>
        ))}
      </Select>
    </FormControl>
  );
};

function FormSelect(props) {
  const { control } = useFormContext();
  const { name, label } = props;
  return (
    <React.Fragment>
      <Controller
        as={MuiSelect}
        control={control}
        name={name}
        label={label}
        defaultValue=""
        {...props}
      />
    </React.Fragment>
  );
}

export default FormSelect;
Enter fullscreen mode Exit fullscreen mode

Things to note in the above code

  1. MuiSelect function is a component which contains our UI for the rendering of the Select field. There are three main props name, label and options. options is an array of objects that contains the data to be displayed in the dropdown.
  2. FormSelect is similar to FormInput component where again we are using useFormContext() method, Controller component and control object.

Let's see how we consume FormSelect in the App.js. Below is the new code in App.js

import React from "react";
import { useForm, FormProvider } from "react-hook-form";
import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";

import FormInput from "./controls/input";
import FormSelect from "./controls/select";

function App(props) {
  const methods = useForm();
  const { handleSubmit } = methods;

  const onSubmit = (data) => {
    console.log(data);
  };

  const numberData = [
    {
      id: "10",
      label: "Ten",
    },
    {
      id: "20",
      label: "Twenty",
    },
    {
      id: "30",
      label: "Thirty",
    },
  ];

  return (
    <div style={{ padding: "10px" }}>
      <Button
        variant="contained"
        color="primary"
        onClick={handleSubmit(onSubmit)}
      >
        SUBMIT
      </Button>

      <div style={{ padding: "10px" }}>
        <FormProvider {...methods}>
          <form>
            <Grid container spacing={2}>
              <Grid item xs={6}>
                <FormInput name="name" label="Name" />
              </Grid>
              <Grid item xs={6}>
                <FormSelect name="sel" label="Numbers" options={numberData} />
              </Grid>
            </Grid>
          </form>
        </FormProvider>
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

What has changed in the App.js

  • I have created data (array of objects) that we will pass to the FormSelect.
  const numberData = [
    {
      id: "10",
      label: "Ten",
    },
    {
      id: "20",
      label: "Twenty",
    },
    {
      id: "30",
      label: "Thirty",
    },
  ];
Enter fullscreen mode Exit fullscreen mode
  • I have added following code to the render
<Grid item xs={6}>
    <FormSelect name="sel" label="Numbers" options={noData} />
</Grid>
Enter fullscreen mode Exit fullscreen mode

Now your web page will look like:
Alt Text

Fill the form data, and click on SUBMIT button. Check the output in the dev-console.

3. Multi-Select with Autocomplete (React-Select)

Here we will be using one of the most popular react libraries React-Select. Create a new folder name select-autocomplete under controls, inside select-autocomplete folder create two files index.js and index.css file Alt Text

Now go to index.js under styles folder and add below code:

export const StyledFormControl = styled(FormControl)`
  && {
    width: 100%;
    display: block;
    position: relative;
  }
`;

export const StyledAutoSelectInputLabel = styled(InputLabel)`
  && {
    position: relative;
    .req-label {
      color: #f44336;
    }
    transform: translate(0, 1.5px) scale(0.75);
    transform-origin: top left;
  }
`;
Enter fullscreen mode Exit fullscreen mode

Now go to index.css under select-autocomplete folder and add below code:

.autoselect-options {
  padding: 6px 16px;
  line-height: 1.5;
  width: auto;
  min-height: auto;
  font-size: 1rem;
  letter-spacing: 0.00938em;
  font-weight: 400;
  cursor: pointer;
}

.autoselect-options:hover {
  background-color: rgba(0, 0, 0, 0.14) !important;
}

Enter fullscreen mode Exit fullscreen mode

I have done the styling changes for two purposes, firstly it will be used when we add validation for error handling and second to make the React-Select look and feel close to Material UI Select.

Now go to index.js under select-autocomplete folder and add below code:

import React, { useEffect, useState } from "react";
import { useFormContext, Controller } from "react-hook-form";
import Select, { createFilter } from "react-select";
import { StyledFormControl, StyledAutoSelectInputLabel } from "../../styles";
import "./index.css";

const stylesReactSelect = {
  clearIndicator: (provided, state) => ({
    ...provided,
    cursor: "pointer",
  }),
  indicatorSeparator: (provided, state) => ({
    ...provided,
    margin: 0,
  }),
  dropdownIndicator: (provided, state) => ({
    ...provided,
    cursor: "pointer",
  }),
  placeholder: (provided, state) => ({
    ...provided,
    fontFamily: "Roboto, Helvetica, Arial, sans-serif",
    color: state.selectProps.error ? "#f44336" : "rgba(0, 0, 0, 0.54)",
  }),
  control: (provided, state) => ({
    ...provided,
    borderRadius: 0,
    border: 0,
    borderBottom: state.selectProps.error
      ? "1px solid #f44336"
      : "1px solid rgba(0,0,0,0.87)",
    boxShadow: "none",
    ":hover": {
      borderColor: state.selectProps.error ? "1px solid #f44336" : "inherit",
      boxShadow: state.selectProps.error ? "1px solid #f44336" : "none",
    },
  }),
  valueContainer: (provided, state) => ({
    ...provided,
    paddingLeft: 0,
  }),
};

const components = {
  Option,
};

function Option(props) {
  const { onMouseMove, onMouseOver, ...newInnerProps } = props.innerProps;
  return (
    <div {...newInnerProps} className="autoselect-options">
      {props.children}
    </div>
  );
}

const ReactSelect = (props) => {
  const { label, options, name } = props;
  return (
    <React.Fragment>
      <StyledFormControl>
        <StyledAutoSelectInputLabel>
          <span>{label}</span>
        </StyledAutoSelectInputLabel>
        <Select
          options={options}
          placeholder="Please Select"
          valueKey="id"
          components={components}
          isClearable={true}
          styles={stylesReactSelect}
          isSearchable={true}
          filterOption={createFilter({ ignoreAccents: false })}
          {...props}
        />
      </StyledFormControl>
    </React.Fragment>
  );
};

function FormSelectAutoComplete(props) {
  const { control } = useFormContext();
  const { name, label, options } = props;

  const [newData, setNewData] = useState([]);

  useEffect(() => {
    const newOptions = options.map((data, index) => ({
      label: data.label,
      value: data.id,
    }));
    setNewData(newOptions);
  }, [options]);

  return (
    <React.Fragment>
      <Controller
        as={ReactSelect}
        name={name}
        control={control}
        label={label}
        {...props}
        options={newData}
      />
    </React.Fragment>
  );
}

export default FormSelectAutoComplete;
Enter fullscreen mode Exit fullscreen mode

Let's break down the code.

const stylesReactSelect = {
  clearIndicator: (provided, state) => ({
    ...provided,
    cursor: "pointer",
  }),
  indicatorSeparator: (provided, state) => ({
    ...provided,
    margin: 0,
  }),
  dropdownIndicator: (provided, state) => ({
    ...provided,
    cursor: "pointer",
  }),
  placeholder: (provided, state) => ({
    ...provided,
    fontFamily: "Roboto, Helvetica, Arial, sans-serif",
    color: state.selectProps.error ? "#f44336" : "rgba(0, 0, 0, 0.54)",
  }),
  control: (provided, state) => ({
    ...provided,
    borderRadius: 0,
    border: 0,
    borderBottom: state.selectProps.error
      ? "1px solid #f44336"
      : "1px solid rgba(0,0,0,0.87)",
    boxShadow: "none",
    ":hover": {
      borderColor: state.selectProps.error ? "1px solid #f44336" : "inherit",
      boxShadow: state.selectProps.error ? "1px solid #f44336" : "none",
    },
  }),
  valueContainer: (provided, state) => ({
    ...provided,
    paddingLeft: 0,
  }),
};
Enter fullscreen mode Exit fullscreen mode
  • The above code is just styling change. As I have mentioned before I have done this to make the look and feel similar to Material UI Select to maintain the design consistency. You can refer to the complete style guide of react-select in this link
const components = {
  Option,
};

function Option(props) {
  const { onMouseMove, onMouseOver, ...newInnerProps } = props.innerProps;
  return (
    <div {...newInnerProps} className="autoselect-options">
      {props.children}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • The above code improves performance if you have a large data(around 100+ data objects)
const ReactSelect = (props) => {
  const { label, options, name } = props;
  return (
    <React.Fragment>
      <StyledFormControl>
        <StyledAutoSelectInputLabel>
          <span>{label}</span>
        </StyledAutoSelectInputLabel>
        <Select
          options={options}
          placeholder="Please Select"
          valueKey="id"
          components={components}
          isClearable={true}
          styles={stylesReactSelect}
          isSearchable={true}
          filterOption={createFilter({ ignoreAccents: false })}
          {...props}
        />
      </StyledFormControl>
    </React.Fragment>
  );
};
Enter fullscreen mode Exit fullscreen mode
  • This is the UI part with label and react-select component. Similar to FormSelect, there are three main props name, label and options. options is an array of objects that contains the data to be displayed in the react-select.
function FormSelectAutoComplete(props) {
  const { control } = useFormContext();
  const { name, label, options } = props;

  const [newData, setNewData] = useState([]);

  useEffect(() => {
    const newOptions = options.map((data, index) => ({
      label: data.label,
      value: data.id,
    }));
    setNewData(newOptions);
  }, [options]);

  return (
    <React.Fragment>
      <Controller
        as={ReactSelect}
        name={name}
        control={control}
        label={label}
        {...props}
        options={newData}
      />
    </React.Fragment>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • FormSelectAutoComplete is similar to FormSelect component where again we are using useFormContext() method, Controller component and control object. One thing to note here is that the array of data objects that are passed to the Select component of react-select should have label and value key in the object. In the below code, I have purposefully passed data that doesn't have this label and value in the object, (which can be the case in the real world scenario) to show you what changes you need to do in order to satisfy this requirement.
useEffect(() => {
    const newOptions = options.map((data, index) => ({
      label: data.label,
      value: data.id,
    }));
    setNewData(newOptions);
  }, [options]);
Enter fullscreen mode Exit fullscreen mode

You don't need to do this if your data object contains label and value as a key.

Let's see how we consume FormSelectAutoComplete in the App.js. Below is the new code in App.js

import React from "react";
import { useForm, FormProvider } from "react-hook-form";
import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";

import FormInput from "./controls/input";
import FormSelect from "./controls/select";
import FormSelectAutoComplete from "./controls/select-autocomplete";

function App(props) {
  const methods = useForm();
  const { handleSubmit } = methods;

  const onSubmit = (data) => {
    console.log(data);
  };

  const numberData = [
    {
      id: "10",
      label: "Ten",
    },
    {
      id: "20",
      label: "Twenty",
    },
    {
      id: "30",
      label: "Thirty",
    },
  ];

  return (
    <div style={{ padding: "10px" }}>
      <Button
        variant="contained"
        color="primary"
        onClick={handleSubmit(onSubmit)}
      >
        SUBMIT
      </Button>

      <div style={{ padding: "10px" }}>
        <FormProvider {...methods}>
          <form>
            <Grid container spacing={2}>
              <Grid item xs={6}>
                <FormInput name="name" label="Name" />
              </Grid>
              <Grid item xs={6}>
                <FormSelect name="sel" label="Numbers" options={numberData} />
              </Grid>
              <Grid item xs={6}>
                <FormSelectAutoComplete
                  name="selAuto"
                  label="Auto Select Numbers"
                  options={numberData}
                  isMulti
                />
              </Grid>
            </Grid>
          </form>
        </FormProvider>
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

What has changed in App.js is the below piece of code

<Grid item xs={6}>
    <FormSelectAutoComplete
      name="selAuto"
      label="Auto Select Numbers"
      options={numberData}
      isMulti
    />
</Grid>
Enter fullscreen mode Exit fullscreen mode

Here we are using the same numberData array of objects that we used in the FormSelect because react-select takes array of objects as data which we have passed in the options prop. isMulti prop is used if we want to show multiple selected values.

Now your web page will look like:
Alt Text

Fill the form data, and click on SUBMIT button. Check the output in the dev-console.

Validation with Yup

If you have a form, the chance is that 99% of the time you will have some sort of the validation. React Hook Forms provide various ways to do the validation (Basic Validaiton & Schema Validation).
We are going to use Yup for our validations.

Let's modify our App.js

import React from "react";
import { useForm, FormProvider } from "react-hook-form";
import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";

import FormInput from "./controls/input";
import FormSelect from "./controls/select";
import FormSelectAutoComplete from "./controls/select-autocomplete";

import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers";

const validationSchema = yup.object().shape({
  nameV: yup.string().required("Name Validation Field is Required"),
  selV: yup.string().required("Select Validation Field is Required"),
  selAutoV: yup.array().required("Multi Select Validation Field required"),
});

function App(props) {
  const methods = useForm({
    resolver: yupResolver(validationSchema),
  });
  const { handleSubmit, errors } = methods;

  const onSubmit = (data) => {
    console.log(data);
  };

  const numberData = [
    {
      id: "10",
      label: "Ten",
    },
    {
      id: "20",
      label: "Twenty",
    },
    {
      id: "30",
      label: "Thirty",
    },
  ];

  return (
    <div style={{ padding: "10px" }}>
      <Button
        variant="contained"
        color="primary"
        onClick={handleSubmit(onSubmit)}
      >
        SUBMIT
      </Button>

      <div style={{ padding: "10px" }}>
        <FormProvider {...methods}>
          <form>
            <Grid container spacing={2}>
              <Grid item xs={6}>
                <FormInput name="name" label="Name" />
              </Grid>
              <Grid item xs={6}>
                <FormInput
                  name="nameV"
                  label="Name with Validation"
                  required={true}
                  errorobj={errors}
                />
              </Grid>
              <Grid item xs={6}>
                <FormSelect name="sel" label="Numbers" options={numberData} />
              </Grid>
              <Grid item xs={6}>
                <FormSelect
                  name="selV"
                  label="Numbers with Validation"
                  options={numberData}
                  required={true}
                  errorobj={errors}
                />
              </Grid>
              <Grid item xs={6}>
                <FormSelectAutoComplete
                  name="selAuto"
                  label="Auto Select Numbers"
                  options={numberData}
                  isMulti
                />
              </Grid>
              <Grid item xs={6}>
                <FormSelectAutoComplete
                  name="selAutoV"
                  label="Auto Select Numbers with Validation"
                  options={numberData}
                  isMulti
                  required={true}
                  errorobj={errors}
                />
              </Grid>
            </Grid>
          </form>
        </FormProvider>
      </div>
    </div>
  );
}

export default App;

Enter fullscreen mode Exit fullscreen mode

Let's dissect the new code changes:

import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers";
Enter fullscreen mode Exit fullscreen mode
  • We have imported yup and yupResolver
const validationSchema = yup.object().shape({
  nameV: yup.string().required("Name Validation Field is Required"),
  selV: yup.string().required("Select Validation Field is Required"),
  selAutoV: yup.array().required("Multi Select Validation Field required"),
});
Enter fullscreen mode Exit fullscreen mode
  • Create a validationSchema object as shown above. nameV is the name of the FormInput field to which the validation needs to be applied. The user input value will be of type "string" hence yup.string(). Since it is a required field yup.string().required(). The custom error message can be passed to required function as shown above. Similarly, selV is the name of FormSelect field, where the value selected from the dropdown will be of type "string" hence yup.string().required(). The custom error message can be passed to required function as shown above. selAutoV is the name of FormSelectAutoComplete field, where the value selected will be in the form of array of objects. Hence yup.array().required(). The custom error message can be passed to required function as shown above.

What if we don't pass a custom error message, it will not throw an error but it will display some other message(Try this out!)

 const methods = useForm({
    resolver: yupResolver(validationSchema),
  });
 const { handleSubmit, errors } = methods;
Enter fullscreen mode Exit fullscreen mode
  • Pass the validationSchema object to the yupResolver function as shown above. Also we will be using errors object from methods object which will contain the field that has an error along with the error message.

  • We have added three new components FormInput, FormSelect & FormSelectAutoComplete with two new props required={true} and errorobj={errors}

<Grid item xs={6}>
    <FormInput
      name="nameV"
      label="Name with Validation"
      required={true}
      errorobj={errors}
    />
 </Grid>
Enter fullscreen mode Exit fullscreen mode
  <Grid item xs={6}>
    <FormSelect
      name="selV"
      label="Numbers with Validation"
      options={numberData}
      required={true}
      errorobj={errors}
    />
  </Grid>
Enter fullscreen mode Exit fullscreen mode
  <Grid item xs={6}>
    <FormSelectAutoComplete
      name="selAutoV"
      label="Auto Select Numbers with Validation"
      options={numberData}
      isMulti
      required={true}
      errorobj={errors}
    />
  </Grid>
Enter fullscreen mode Exit fullscreen mode

Now we need to modify our FormInput, FormSelect & FormSelectAutoComplete component to highlight validation error and show respective error messages.
FormInput

  • Create an index.css file in the input folder of controls (controls -> input -> index.css). index.css will have following code:
.required-label span {
    color: #f44336;
  }
Enter fullscreen mode Exit fullscreen mode
import React from "react";
import { useFormContext, Controller } from "react-hook-form";
import TextField from "@material-ui/core/TextField";
import "./index.css";

function FormInput(props) {
  const { control } = useFormContext();
  const { name, label, required, errorobj } = props;
  let isError = false;
  let errorMessage = "";
  if (errorobj && errorobj.hasOwnProperty(name)) {
    isError = true;
    errorMessage = errorobj[name].message;
  }

  return (
    <Controller
      as={TextField}
      name={name}
      control={control}
      defaultValue=""
      label={label}
      fullWidth={true}
      InputLabelProps={{
        className: required ? "required-label" : "",
        required: required || false,
      }}
      error={isError}
      helperText={errorMessage}
      {...props}
    />
  );
}

export default FormInput;
Enter fullscreen mode Exit fullscreen mode

We have made the following changes:

const { name, label, required, errorobj } = props;
  let isError = false;
  let errorMessage = "";
  if (errorobj && errorobj.hasOwnProperty(name)) {
    isError = true;
    errorMessage = errorobj[name].message;
  }
Enter fullscreen mode Exit fullscreen mode

The required and errorobj which were passed as props to the FormInput component in App.js are being used above. errorObj consist of name of the field and error message which we have passed in the validation schema. This object is created by react hook forms. The above piece of code will be similar across FormSelect & FormSelectAutoComplete form components which we have created.

Next change which we did was to the Controller component

    <Controller
      as={TextField}
      name={name}
      control={control}
      defaultValue=""
      label={label}
      fullWidth={true}
      InputLabelProps={{
        className: required ? "required-label" : "",
        required: required || false,
      }}
      error={isError}
      helperText={errorMessage}
      {...props}
    />
Enter fullscreen mode Exit fullscreen mode

We added the following new props to the Controller component.

InputLabelProps={{
    className: required ? "required-label" : "",
    required: required || false,
}}
error={isError}
helperText={errorMessage}
Enter fullscreen mode Exit fullscreen mode

InputLabelProps, error and helperText props are specified by Material UI TextField to control the styling of TextField and how to show an error message.

Similar code changes will be done to FormSelect and FormSelectAutoComplete component.
FormSelect

import React from "react";
import { useFormContext, Controller } from "react-hook-form";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select";
import { StyledInputLabel } from "../../styles";
import FormHelperText from "@material-ui/core/FormHelperText";

const MuiSelect = (props) => {
  const { label, name, options, required, errorobj } = props;
  let isError = false;
  let errorMessage = "";
  if (errorobj && errorobj.hasOwnProperty(name)) {
    isError = true;
    errorMessage = errorobj[name].message;
  }

  return (
    <FormControl fullWidth={true} error={isError}>
      <StyledInputLabel htmlFor={name}>
        {label} {required ? <span className="req-label">*</span> : null}
      </StyledInputLabel>
      <Select id={name} {...props}>
        <MenuItem value="">
          <em>None</em>
        </MenuItem>
        {options.map((item) => (
          <MenuItem key={item.id} value={item.id}>
            {item.label}
          </MenuItem>
        ))}
      </Select>
      <FormHelperText>{errorMessage}</FormHelperText>
    </FormControl>
  );
};

function FormSelect(props) {
  const { control } = useFormContext();
  const { name, label } = props;
  return (
    <React.Fragment>
      <Controller
        as={MuiSelect}
        control={control}
        name={name}
        label={label}
        defaultValue=""
        {...props}
      />
    </React.Fragment>
  );
}

export default FormSelect;
Enter fullscreen mode Exit fullscreen mode

FormSelectAutoComplete

import React, { useEffect, useState } from "react";
import { useFormContext, Controller } from "react-hook-form";
import Select, { createFilter } from "react-select";
import { StyledFormControl, StyledAutoSelectInputLabel } from "../../styles";
import FormHelperText from "@material-ui/core/FormHelperText";
import "./index.css";

const stylesReactSelect = {
  clearIndicator: (provided, state) => ({
    ...provided,
    cursor: "pointer",
  }),
  indicatorSeparator: (provided, state) => ({
    ...provided,
    margin: 0,
  }),
  dropdownIndicator: (provided, state) => ({
    ...provided,
    cursor: "pointer",
  }),
  placeholder: (provided, state) => ({
    ...provided,
    fontFamily: "Roboto, Helvetica, Arial, sans-serif",
    color: state.selectProps.error ? "#f44336" : "rgba(0, 0, 0, 0.54)",
  }),
  control: (provided, state) => ({
    ...provided,
    borderRadius: 0,
    border: 0,
    borderBottom: state.selectProps.error
      ? "1px solid #f44336"
      : "1px solid rgba(0,0,0,0.87)",
    boxShadow: "none",
    ":hover": {
      borderColor: state.selectProps.error ? "1px solid #f44336" : "inherit",
      boxShadow: state.selectProps.error ? "1px solid #f44336" : "none",
    },
  }),
  valueContainer: (provided, state) => ({
    ...provided,
    paddingLeft: 0,
  }),
};

const components = {
  Option,
};

function Option(props) {
  const { onMouseMove, onMouseOver, ...newInnerProps } = props.innerProps;
  return (
    <div {...newInnerProps} className="autoselect-options">
      {props.children}
    </div>
  );
}

const ReactSelect = (props) => {
  const { label, options, required, errorobj, name } = props;
  let isError = false;
  let errorMessage = "";
  if (errorobj && errorobj.hasOwnProperty(name)) {
    isError = true;
    errorMessage = errorobj[name].message;
  }
  return (
    <React.Fragment>
      <StyledFormControl>
        <StyledAutoSelectInputLabel>
          <span className={isError ? "req-label" : ""}>
            {label} {required ? <span className="req-label">*</span> : null}
          </span>
        </StyledAutoSelectInputLabel>
        <Select
          options={options}
          placeholder="Please Select"
          valueKey="id"
          components={components}
          isClearable={true}
          styles={stylesReactSelect}
          isSearchable={true}
          filterOption={createFilter({ ignoreAccents: false })}
          error={isError}
          {...props}
        />
        {isError && (
          <FormHelperText error={isError}>{errorMessage}</FormHelperText>
        )}
      </StyledFormControl>
    </React.Fragment>
  );
};

function FormSelectAutoComplete(props) {
  const { control } = useFormContext();
  const { name, label, options } = props;

  const [newData, setNewData] = useState([]);

  useEffect(() => {
    const newOptions = options.map((data, index) => ({
      label: data.label,
      value: data.id,
    }));
    setNewData(newOptions);
  }, [options]);

  return (
    <React.Fragment>
      <Controller
        as={ReactSelect}
        name={name}
        control={control}
        label={label}
        defaultValue={[]}
        {...props}
        options={newData}
      />
    </React.Fragment>
  );
}

export default FormSelectAutoComplete;
Enter fullscreen mode Exit fullscreen mode

Save the code, run the app and click on SUBMIT button. Your webpage will look like
Alt Text

Pre populating form field data

There is always a scenario, where the form fields need to be pre-populated with some data e.g An Edit case of a web form.
React Hook Forms provide us with a method setValue to do that.

setValue("name", "Ammar");
Enter fullscreen mode Exit fullscreen mode
  • Here setValue is the function which accepts two parameters. name is the name of the field, "Ammar" is the value of the field to be set.
  • setValue function comes from method object of useForm function.
 const methods = useForm();
 const {setValue} = methods;
Enter fullscreen mode Exit fullscreen mode

Github repo

I have created few more form components like Date Picker, Radio Buttons and Checkbox and have also shown validation for the date as well. Also, all of the code in this tutorial is present in the repo. You can use this repo as a reference or directly use the code in your project.
Repo

References

If you have any queries/suggestion or found some issue with what I have explained in this article please let me know in the comments below. Thank you for your time in reading my article.

Top comments (10)

Collapse
 
resurii14 profile image
Leslie

Thanks for sharing this detailed guide! I have just discovered react-hook-form. I haven't tried it out yet, but reading from the docs and tutorials, I like that it doesn't re-render as much as other libraries do. The re-rendering part is something I wished was addressed "out of the box" in other libraries.

You said that you have forms containing many fields. Do you also use 3rd-party UI integrations there?

Collapse
 
ammartinwala52 profile image
Ammar Tinwala

Hi @leslie

Apologies for my very very late response.

Yes I'm using the 3rd party UI integrations. This tutorial is my actual workflow in the projects that I have developed and they are being used by my clients in production.
3rd party UI libraries that I have used is : Material UI, React-Select and Material-UI pickers dev. (Date and Time)

If you go to my github repo there you will find all the above mentioned integration.
Let me know if you need some more info/help in these.

I thank you very much for your time in going through my article.

Collapse
 
haase1020 profile image
Mandi Haase

Thank you for your post! I am having an issue with @hookform/resolvers in the src folder. I am getting the following error and would love advice on how to troubleshoot. I have installed @hookform/resolvers: Module not found: Can't resolve '@hookform/resolvers' in '/home/mandi/mandi/material-ui-reacthookform/src'

Collapse
 
swalker326 profile image
Shane Walker

Should be

import { yupResolver } from "@hookform/resolvers/yup";

Collapse
 
ndrean profile image
NDREAN • Edited

Thanks for this. How would you integrate the register method? I found, use inputRef={register} in the TextField, no need for Controller.

Collapse
 
ammartinwala52 profile image
Ammar Tinwala

Hi Ndrean

Thank you for your time in reading this. I apologize for my very very delayed response.

register method can be used with any UI element that exposes a ref for register method. The reason I used Controller is because if you are using a Controlled component, then your workflow becomes very easy to integrate.

The form controls that I have created you can easily swap it with any other 3rd party UI library instead of Material UI and the core logic with "React Hooks Form" library will remain the same.
With this view in mind I have used the Controller component.

Collapse
 
batamire profile image
batamire

Check out this wicked implementation: github.com/redwoodjs/redwood/blob/...

Redwood forms guide:
redwoodjs.com/tutorial/everyone-s-...

Collapse
 
snruline profile image
snruline

Select How to use onChange?

Collapse
 
snruline profile image
snruline

const { label, name, options, required, errorobj, onChange } = props;
onChange={(e) => {
onChange(e)
}}

Collapse
 
ammartinwala52 profile image
Ammar Tinwala

Hi Snruline

I'm very very sorry for the delayed response.
Can you please elaborate in detail where do you want to use onChange ?