Mohammad Faisal I am a full-stack software engineer working with the MERN stack. Also, I am a blogger in my free time and love to write about various technical topics.

Using Material UI with React Hook Form

8 min read 2410

React Hook Form is one of the most popular libraries for handling form inputs in the React ecosystem. Getting it to work properly can be tricky if you’re using a component library such as Material UI.

In this guide, we’ll demonstrate how to use Material UI with React Hook Form. This tutorial is also helpful if you want to integrate some other React UI library, such as Ant Design or Semantic UI.

To follow along, you should already have some exposure to Material UI and React Hook Form. We won’t dive too deep into how to use those libraries. Instead, we’ll focus on the integration between them.

To show how to use Material UI with React Hook Form, we’re going to build a complete form with the most-used input components provided by Material UI, including:

  • Text input
  • Radio input
  • Dropdown
  • Date
  • Checkbox
  • Slider

The form will also have reset functionality. It will look like this:

Material UI and React Hook Form Example

If you’re more of a visual learner, check out the accompanying video tutorial:

Text input component

Let’s start with a simple form component. This component will have only one text input in it.

To build this form by the traditional approach without any library, we need to handle the change of input separately. We also have to take care of the reset functionality and validation ourselves.

We made a custom demo for .
No really. Click here to check it out.

It will probably look something like this:

import TextField from "@material-ui/core/TextField";
import React, { useState } from "react";
import { Button, Paper } from "@material-ui/core";

export const FormWithoutHookForm = () => {
  const [textValue, setTextValue] = useState<string>("");

  const onTextChange = (e: any) => setTextValue(e.target.value);
  const handleSubmit = () => console.log(textValue);
  const handleReset = () => setTextValue("");

  return (
    <Paper>
      <h2>Form Demo</h2>

      <TextField
        onChange={onTextChange}
        value={textValue}
        label={"Text Value"} //optional
      />

      <Button onClick={handleSubmit}>Submit</Button>
      <Button onClick={handleReset}>Reset</Button>
    </Paper>
  );
};

The output will look like this:

Material UI and React Hook Form Text Input Component Example

Here we are storing the value by using the useState Hook provided by React itself:

const [textValue, setTextValue] = useState<string>("");

Also, we’re setting the value from the input in our onTextChange function:

const onTextChange = (e: any) => setTextValue(e.target.value);

If we look at the TextInput component provided by material-ui, we can see there are two \ important props passed to it: value and onChange. value takes care of the actual value of the input, while onChange determines what happens when the input changes. However we use this form, we need to take care of these two things.

Setting up React Hooks Form

React Hook Form exports some utility from the famous useForm Hook, which you then use inside your input components.

First, import the useForm Hook:

import { useForm } from "react-hook-form";

Then, use the Hook inside the component:

const { register } = useForm();

A typical input might look like this:

<input type="text" ref={register} name="firstName" />

Look closely here: we passed the register as a value to the ref of the actual input component. All the magic happens behind the scenes.

reactstrap provides a similar prop named innerRef, which can be used to pass our register for seamless integration with react-hook-form.

Unfortunately this is not the case when we use Material UI; the library does not yet provide any similar prop to pass the register as a value to the ref prop.

The Controller component

React Hook Form includes a wrapper component called Controller to work with component libraries where you can’t access the ref directly.

According to the React docs, this is a render prop — a function that returns a React element and provides the ability to attach events and value into the component.

The skeleton of this special Controller component is as follows:

<Controller
  control={control}
  name="test"
  render={({
    field: { onChange, onBlur, value, name, ref },
    fieldState: { invalid, isTouched, isDirty, error },
    formState,
  }) => ( WHATEVER_INPUT_WE_WANT )}
/>

Let’s break down what’s going on here:

  • control is a prop that we get back from the useForm Hook and pass into the input
  • name is how React Hook Form tracks the value of an input internally
  • render is the most important prop; we pass a render function here

The render prop

The render property of Controller is the most important prop to understand. The function has three keys: field , fieldState, and formState. We’re going to focus on field for now.

The field object exports two things (among others): value and onChange. We have already seen that we need these two things to control almost any input.

Refactoring our form

So let’s see if the Controller component really solves our problems. We’ll use the Controller component and pass the TextInput inside the render function.

Let’s first extract out what we need from the useForm Hook:

const { handleSubmit, reset, control } = useForm();

Then, use the Controller component in the form like so:

import TextField from "@material-ui/core/TextField";
import React, { useState } from "react";
import { Button, Paper } from "@material-ui/core";
import { Controller, useForm } from "react-hook-form";

export const FormWithHookForm = () => {
  const { handleSubmit, reset, control } = useForm();
  const onSubmit = (data: any) => console.log(data);

  return (
    <form>
      <Controller
        name={"textValue"}
        control={control}
        render={({ field: { onChange, value } }) => (
          <TextField onChange={onChange} value={value} label={"Text Value"} />
        )}
      />
      <Button onClick={handleSubmit(onSubmit)}>Submit</Button>
      <Button onClick={() => reset()} variant={"outlined"}>Reset</Button>
    </form>
  );
};

This form works just like the previous one. The magic happens thanks to the field property of the render function provided by the Controller.

Extracting a component to make it reusable

So we now know how to use the Controller component of React Hook Form to get the form to work without any ref. Now let’s extract the input component to a separate component so we can use it everywhere.

This common component will need three props from its parent:

  • name, the key for the input
  • control which is used to get access to the functionalities of React Hook Form
  • label, the label for the input (optional)
import TextField from "@material-ui/core/TextField";
import { Controller } from "react-hook-form";
import React from "react";

export const FormInputText = ({ name, control, label }) => {
  return (
     (
        
      )}
    />
  );
};

We’ll use this component in our form like so:

import { FormInputText } from "./FormInputTextGood";

export const FormWithHookForm = () => {
  // rest are same as before
  return (
    <form>
      <FormInputText
        name={"textInput"}
        control={control}
        label={"Text Input"}
      />
    </form>
  );
};

Now the component is much easier to understand and reuse. Let’s take care of some other inputs as well.

The Radio input component

The second-most common input component is Radio. There is an important concept to remember here.

If you have used Radio from Material UI, you already know that you need the RadioGroup component as the parent and a bunch of options inside, such as individual Radio buttons, as children:

import React from "react";
import {
  FormControl,
  FormControlLabel,
  FormLabel,
  Radio,
  RadioGroup,
} from "@material-ui/core";
import { Controller, useFormContext } from "react-hook-form";
import { FormInputProps } from "./FormInputProps";

const options = [
  {
    label: "Radio Option 1",
    value: "1",
  },
  {
    label: "Radio Option 2",
    value: "2",
  },
];

export const FormInputRadio: React.FC<FormInputProps> = ({ name,control,label }) => {
  const generateRadioOptions = () => {
    return options.map((singleOption) => (
      <FormControlLabel
        value={singleOption.value}
        label={singleOption.label}
        control={<Radio />}
      />
    ));
  };  

  return <Controller
      name={name}
      control={control}
      render={({field: { onChange, value }}) => (
        <RadioGroup value={value} onChange={onChange}>
          {generateRadioOptions()}
        </RadioGroup>
      )}
    />
};

The main concept is the same here. We just use the onChange and value from the render functions field object and pass it to the RadioGroup.

Note that we didn’t use the label here. If you want to use that, you will need to add the FormControl and FormLabel components of Material UI.

Also note that used a special function, generateRadioOptions, to generate the individual radio inputs. We added the options as a constant inside the component. You can have them as props or any other way you see fit.

Almost any form needs some kind of dropdown. The code for the Dropdown component is as follows:

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

const options = [
  {
    label: "Dropdown Option 1",
    value: "1",
  },
  {
    label: "Dropdown Option 2",
    value: "2",
  },
];

export const FormInputDropdown= ({name,control, label}) => {

  const generateSelectOptions = () => {
    return options.map((option) => {
      return (
        <MenuItem key={option.value} value={option.value}>
          {option.label}
        </MenuItem>
      );
    });
  };

  return <Controller
      control={control}
      name={name}
      render={({ field: { onChange, value } }) => (
        <Select onChange={onChange} value={value}>
          {generateSelectOptions()}
        </Select>
      )}
    />
};

Date input

This is a common yet special component. In Material UI, we don’t have any Date component that works out of the box. We need to leverage some helper libraries.

First, install those dependencies:

yarn add @date-io/[email protected] @material-ui/[email protected] [email protected]22.1

Be careful about the versions. Otherwise, you may run into some weird issues.

We also need to wrap our data input component with a special wrapper, MuiPickersUtilsProvider. This will inject the date picker functionality for us.

Remember, this is not a requirement for React Hook Form; it’s required by Material UI. So if you’re using any other design library, such as Ant Design or Semantic UI, you don’t need to worry about it.

import React from "react";
import DateFnsUtils from "@date-io/date-fns";
import { KeyboardDatePicker, MuiPickersUtilsProvider} from "@material-ui/pickers";
import { Controller } from "react-hook-form";
const DATE_FORMAT = "dd-MMM-yy";

export const FormInputDate = ({ name, control, label }) => {
  return (
    <MuiPickersUtilsProvider utils={DateFnsUtils}>
      <Controller
        name={name}
        control={control}
        render={({ field : {onChange , value } }) => (
          <KeyboardDatePicker
            onChange={onChange}
            value={value}
            label={label}
          />
        )}
      />
    </MuiPickersUtilsProvider>
  );
};

Checkbox group

If you want to use a simple checkbox that works like a switch, then it’s easy to use. You just need to use it like the previous components, so I won’t show the same thing once again.

However, complexities arise when you want to create a group of checkboxes and set the selected values as an array of selected items. The main challenge here is that Material UI doesn’t provide a multiselect checkbox component.

There is no clear example of how to use this component with React Hook Form. To enable this functionality, we have to maintain a local state of selected items:

import React, { useEffect, useState } from "react";
import {Checkbox,FormControl,FormControlLabel,FormLabel} from "@material-ui/core";
import { Controller } from "react-hook-form";

const options = [
  {
    label: "Checkbox Option 1",
    value: "1",
  },
  {
    label: "Checkbox Option 2",
    value: "2",
  },
];

export const FormInputMultiCheckbox= ({name,control,setValue,label}) => {
  const [selectedItems, setSelectedItems] = useState<any>([]);

  // we are handling the selection manually here
  const handleSelect = (value: any) => {
    const isPresent = selectedItems.indexOf(value);
    if (isPresent !== -1) {
      const remaining = selectedItems.filter((item: any) => item !== value);
      setSelectedItems(remaining);
    } else {
      setSelectedItems((prevItems: any) => [...prevItems, value]);
    }
  };

  // we are setting form value manually here
  useEffect(() => {
    setValue(name, selectedItems); 
  }, [selectedItems]);

  return (
    <FormControl size={"small"} variant={"outlined"}>
      <FormLabel component="legend">{label}</FormLabel>

      <div>
        {options.map((option: any) => {
          return (
            <FormControlLabel
              control={
                <Controller
                  name={name}
                  render={({}) => {
                    return (
                      <Checkbox
                        checked={selectedItems.includes(option.value)}
                        onChange={() => handleSelect(option.value)}
                      />
                    );
                  }}
                  control={control}
                />
              }
              label={option.label}
              key={option.value}
            />
          );
        })}
      </div>
    </FormControl>
  );
};

So in this component we are controlling the value and onChange both manually here. That’s why inside the render function we are not using the field prop anymore. To set the value we are taking another new prop named setValue here. This function is a special function of react-hook-form for setting the value manually.

You can ask then why are we doing this if we are manually handling the inputs? The answer is when you are using react-hook-form you want all your inputs in one place. So we are giving this MultiSelectCheckbox component a special treatment here so that it works with other components easily.

Slider

Our final component is a Slider component, which is a fairly common component.

The code is simple to understand, but there is one catch: the onChange function provided by Material UI does not work with the onChange of React Hook Form because the signature is different.

As a result, when we try to use the Slider component inside a Controller component from React Hook Form, it throws error. Once again, we must maintain a local state to control the onChange and set the value manually.

The complete code for this component is as follows:

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

export const FormInputSlider = ({name,control,setValue,label}) => {
  const [sliderValue, setSliderValue] = React.useState(0);

  useEffect(() => {
    if (sliderValue) setValue(name, sliderValue);
  }, [sliderValue]);

  const handleChange = (event: any, newValue: number | number[]) => {
    setSliderValue(newValue as number);
  };

  return <Controller
      name={name}
      control={control}
      render={({ field, fieldState, formState }) => (
        <Slider
          value={sliderValue}
          onChange={handleChange}
        />
      )}
    />
};

Putting it all together

Now let’s use all of these components inside our form. Our form will take advantage of all the reusable components we just made:

import { Button, Paper, Typography } from "@material-ui/core";
import { FormProvider, useForm } from "react-hook-form";
import { FormInputText } from "./form-components/FormInputText";
import { FormInputMultiCheckbox } from "./form-components/FormInputMultiCheckbox";
import { FormInputDropdown } from "./form-components/FormInputDropdown";
import { FormInputDate } from "./form-components/FormInputDate";
import { FormInputSlider } from "./form-components/FormInputSlider";
import { FormInputRadio } from "./form-components/FormInputRadio";

const defaultValues = {
  textValue: "",
  radioValue: "",
  checkboxValue: [],
  dateValue: new Date(),
  dropdownValue: "",
  sliderValue: 0,
};

export const FormDemo = () => {
  const methods = useForm({ defaultValues: defaultValues });
  const { handleSubmit, reset, control, setValue } = methods;
  const onSubmit = (data: IFormInput) => console.log(data);

  return (
    <Paper>
      <Typography variant="h6"> Form Demo </Typography>

      <FormInputText name="textValue" control={control} label="Text Input" />
      <FormInputRadio name={"radioValue"} control={control} label={"Radio Input"} />
      <FormInputDropdownname="dropdownValue"control={control}label="Dropdown Input"/>
      <FormInputDate name="dateValue" control={control} label="Date Input" />

      <FormInputMultiCheckbox
        control={control}
        setValue={setValue}
        name={"checkboxValue"}
        label={"Checkbox Input"}
      />

      <FormInputSlider
        name={"sliderValue"}
        control={control}
        setValue={setValue}
        label={"Slider Input"}
      />

      <Button onClick={handleSubmit(onSubmit)} variant={"contained"}>
        Submit
      </Button>
      <Button onClick={() => reset()} variant={"outlined"}>
        Reset
      </Button>
    </Paper>
  );
};

Conclusion

Now our form is much cleaner and more performant. From here, we can add our form validation logic and error handling very easily.

To play around with this example on your own, check out the complete code on GitHub.

Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — .

Mohammad Faisal I am a full-stack software engineer working with the MERN stack. Also, I am a blogger in my free time and love to write about various technical topics.

Testing accessibility with Storybook

One big challenge when building a component library is prioritizing accessibility. Accessibility is usually seen as one of those “nice-to-have” features, and unfortunately, we’re...
Laura Carballo
4 min read

Leave a Reply