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:
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.
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:
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 theuseForm
Hook and pass into the inputname
is how React Hook Form tracks the value of an input internallyrender
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 inputcontrol
which is used to get access to the functionalities of React Hook Formlabel
, 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.
Dropdown
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]
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.
More great articles from LogRocket:
- Don't miss a moment with The Replay, a curated newsletter from LogRocket
- Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
- Use React's useEffect to optimize your application's performance
- Switch between multiple versions of Node
- Discover how to animate your React app with AnimXYZ
- Explore Tauri, a new framework for building binaries
- Advisory boards aren’t just for executives. Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
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.
LogRocket: Full visibility into your 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 combines session replay, product analytics, and error tracking – empowering software teams to create the ideal web and mobile product experience. What does that mean for you?
Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay problems as if they happened in your own browser to quickly understand what went wrong.
No more noisy alerting. Smart error tracking lets you triage and categorize issues, then learns from this. Get notified of impactful user issues, not false positives. Less alerts, way more useful signal.
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 — start monitoring for free.
Very useful, when I downloaded the project I got it all, I like it a lot
I changed the FormInputText because there is no code in the return.
Sharing my result. Thank u. Nice article!
import TextField from ‘@mui/material/TextField’;
import { Controller } from “react-hook-form”;
export const FormInputText = ({ name, control, label }) => {
return (
}
/>
)
}
Thanks for the writeup. I’m a bit baffled on your approach with hook-form, how come you opted to make the control approach for these? I would have preferred the register method much more for known frameworks that behave nicely with hook-form. Sure, in case where specific control is needed I see the controller as the choice, but for most of the examples register would have entirely sufficed. Once more, thanks for the writeup, I seem to return to your texts often enough.
What I’ve seen is that with Material UI you can’t do register because MUI components are controlled components, so you have to wrap a controller around them. It feels really messy to do it to be honest.
That isn’t true. There might be some quirks but something like
Definitely works.
You can register the underlying input.
https://mui.com/api/text-field/ -> inputProps
Can’t we just create a simple FormField component instead of creating FormInputText, FormInputSlider etc. ?
You can use something like `https://github.com/adiathasan/mui-react-hook-form-plus`
This package “@material-ui/[email protected]” is deprecated, and what is the alternate of this package.
Thank you for the published content. It is extremely important because this is a difficulty that many devs face when controlling inputs from other libraries.