Using MUI with React Hook Form
Connect MUI TextField, Select, Autocomplete, Checkbox and DatePicker to react-hook-form without a Controller on every field, with validation and error text.
MUI's inputs are controlled components, so React Hook Form's register doesn't work with
them directly. The standard fix is a Controller around every field, which works but repeats the
same mapping each time. This guide shows that pattern, then the same form built with
@piplup/rhf-adapters, which does the mapping once per component type.
The Controller way
Here is one MUI TextField connected to React Hook Form with a Controller:
<Controller
name="email"
control={control}
rules={{ required: "Enter your email" }}
render={({ field, fieldState }) => (
<TextField
{...field}
inputRef={field.ref}
label="Email"
error={!!fieldState.error}
helperText={fieldState.error?.message}
/>
)}
/>Every field needs the same four decisions: spread the field props, route ref to the inner
input so React Hook Form can focus it on error, turn the error into the error flag, and show
its message as helper text. Select, Autocomplete, Checkbox and the date pickers each need a
slightly different version of that mapping, because their onChange signatures and empty values
differ.
The adapter way
The same field with MuiTextFieldElement:
<MuiTextFieldElement
control={control}
name="email"
label="Email"
required
messages={{ required: "Enter your email" }}
/>The element calls useController for you and handles the mapping above. It also accepts
required, min, max, minLength, maxLength and pattern as props, and turns them into
React Hook Form rules with readable default messages.
Install the core package and the adapters alongside MUI:
npm install @piplup/rhf-core @piplup/rhf-adapters react-hook-form @mui/material @emotion/react @emotion/styledThe installation page lists the supported versions. The date pickers live in a separate subpath with their own installation page.
A complete form
import FormControl from "@mui/material/FormControl";
import InputLabel from "@mui/material/InputLabel";
import MenuItem from "@mui/material/MenuItem";
import {
MuiButtonElement,
MuiFormHelperTextElement,
MuiSelectElement,
MuiTextFieldElement,
} from "@piplup/rhf-adapters/mui-material";
import { useForm } from "react-hook-form";
type SignUp = { name: string; email: string; role: string };
export function SignUpForm() {
const { control, handleSubmit } = useForm<SignUp>({
defaultValues: { name: "", email: "", role: "" },
});
return (
<form noValidate onSubmit={handleSubmit((values) => console.log(values))}>
<MuiTextFieldElement control={control} name="name" label="Name" required />
<MuiTextFieldElement control={control} name="email" type="email" label="Email" required />
<FormControl fullWidth>
<InputLabel id="role-label">Role</InputLabel>
<MuiSelectElement control={control} name="role" labelId="role-label" label="Role" required>
<MenuItem value="viewer">Viewer</MenuItem>
<MenuItem value="editor">Editor</MenuItem>
</MuiSelectElement>
<MuiFormHelperTextElement control={control} name="role" renderOnError />
</FormControl>
<MuiButtonElement control={control} type="submit" variant="contained">
Sign up
</MuiButtonElement>
</form>
);
}Three details in this form are easy to miss:
noValidateon the form.requiredis passed through to the input, which turns on the browser's own validation popup.noValidatelets React Hook Form's messages show instead.type="email"adds an email pattern with the message "Please enter an email address." You don't need to write the regular expression yourself.MuiFormHelperTextElementunder the Select. MUI'sSelecthas nohelperTextprop, so the Select's error message is rendered by a separate element that reads the same field. WithrenderOnErrorit renders nothing until there is an error.
Without useForm: FormContainer
FormContainer creates the form, provides it through
context and adds noValidate, so the elements don't need a control prop:
import { MuiButtonElement, MuiTextFieldElement } from "@piplup/rhf-adapters/mui-material";
import { FormContainer } from "@piplup/rhf-core";
export function EmailForm() {
return (
<FormContainer defaultValues={{ email: "" }} onSubmit={(values) => console.log(values)}>
<MuiTextFieldElement name="email" type="email" label="Email" required />
<MuiButtonElement type="submit" variant="contained">
Save
</MuiButtonElement>
</FormContainer>
);
}Validation
Every element accepts these props and turns them into React Hook Form rules:
| Prop | Rule | Default message |
|---|---|---|
required | required | Please fill out this field. |
minLength | minLength | Please lengthen this text to n characters or more |
maxLength | maxLength | Please shorten this text to n characters or less. |
min | min | Value must be n or later. |
max | max | Value must be n or earlier. |
pattern | pattern | Please match the requested format. |
Override any message with the messages prop, for example
messages={{ min: "You must be 18 or older" }}. The min and max defaults are worded for
dates, so a custom message reads better on numbers.
For anything else, pass React Hook Form's own rules, such as a validate function. A rule in
rules wins over the shorthand prop for the same rule.
Schema validation with zod or yup works the usual way, through a resolver on useForm. Resolver
errors land in the same field state, so they show as helper text too. Choose one approach per
form: with a resolver, React Hook Form ignores field-level rules, including the ones these props
create.
Default and empty values
Each element normalizes the empty value its MUI component expects. Set defaultValues to match,
so every field starts controlled:
| Element | Empty value | Stored value |
|---|---|---|
MuiTextFieldElement | "" | The string; with type="number", a number or null |
MuiSelectElement | "", or [] with multiple | The selected value, or an array of them |
MuiAutocompleteElement | null, or [] with multiple | The whole option, or an array of options |
MuiCheckboxElement | []; unchecking every box stores undefined | An array of the checked boxes' values |
MuiXDatePickerElement | null | A date object from your LocalizationProvider adapter |
When the stored shape should differ from what the component shows, use transform. For example,
the Autocomplete hook demo
stores a country code while the input shows the country object. A checkbox can store a boolean
with a one-line transform, shown on the
MuiCheckboxElement page.
When you need your own component
Every element has a matching hook, such as
useMuiTextFieldAdapter, that
returns the props instead of rendering. Use the hook when the field needs layout or behaviour the
element doesn't offer. For a component library that has no adapter yet, build one on
useControllerAdapter from @piplup/rhf-core.
Every MUI element
- Text inputs: TextField, Input, OutlinedInput, FilledInput, InputBase
- Choices: Select, Autocomplete, Checkbox, RadioGroup, Radio, Switch, Slider, Rating
- Dates and times: DatePicker, TimePicker, DateTimePicker, and the rest of MUI X
- Labels, errors and buttons: FormHelperText, FormLabel, InputLabel, Button
- MUI community inputs: chips, color, file, OTP, phone number
Next steps
- Date pickers with React Hook Form: loading, submitting and validating dates and date ranges.
- rhf-adapters vs react-hook-form-mui: how this compares with the other popular MUI integration.
FAQ
Do I need a Controller to use MUI with React Hook Form?
MUI inputs are controlled components, so react-hook-form's register doesn't fit them well and the usual answer is a Controller per field. The Mui*Element components wrap useController for you, so you pass control and name and skip the Controller and its render function.
Why does the browser show its own validation popup instead of my error message?
Elements pass required through to the input, which turns on the browser's built-in validation. Add noValidate to your <form>, or use FormContainer, which adds it for you, and react-hook-form's messages show instead.
Can I use zod or yup instead of the validation props?
Yes. Pass a resolver to useForm as usual. Resolver errors land in the same field state the elements read, so the message still shows as the field's helper text. With a resolver, react-hook-form ignores field-level rules, so put all validation in the schema; required on an element then only adds the asterisk.
How do I change the default error messages for every field?
Each field takes a messages prop for its own rules. To format every error in one place, wrap the form in FormErrorProvider with an errorParser, or pass errorParser to FormContainer. A parser from context takes precedence over one passed to a single field.
Does it work with MUI X date pickers?
Yes. @piplup/rhf-adapters/mui-x-date-pickers has an element for every MUI X picker, plus the date and time fields, calendars and clocks. Picker limits such as minDate and disablePast also become react-hook-form validation, each with its own message.
Forms
Bind react-hook-form to MUI, MUI X date pickers and native inputs without writing the same Controller boilerplate on every field.
Date pickers with React Hook Form
Use MUI X date pickers in react-hook-form: set up the picker, load dates from an API, submit them in the right format, validate a date range and use Zod.