Are you confused in how to make the Select of MaterialUI works fine with React Hooks Form?
What does not work: use directly the Select component ☹️
The most advantage of use React Hook Form is to get all the benefits of work with uncontrolled components in forms like skip re-rendering and faster mount. But there are some limitations in use some components that are created to work internally like a controlled, this is the case of component Select of Material UI. In this scenario the documentation o React Hook Form indicate to use the Controller component, but there's some other ways to do the Select component work correctly without Controller component.
Lets get started with a simple Select of persons of trinity: Father, Son and Holy Spirit.
const trinityPersons = [
{ value: "father", text: "Father" },
{ value: "son", text: "Son" },
{ value: "spirit", text: "Holy Spirit" },
];
Let's import some things
import { FormControl, TextField, MenuItem } from "material-ui/core";
import { useForm } from "react-hooks-form";
First way: with Textfield component
In this first way is we can save some lines of code only working directly with the TextField component and add the select prop to made our input work like an select. Now through the prop inputProps that Material UI provide to us we can add prop directly to the select input component. Magic!
The next step is check if the ref already exist to prevent error and then create our register provided by React Hook Form. This is the more shortest way because we prevent to use the Controller and Select component directly.
return(
<FormControl>
<TextField
select
label="Choose one Person of trinity"
id="trinity"
inputProps={{
inputRef: (ref) => {
if (!ref) return;
register({
name: "trinityPerson",
value: ref.value,
});
},
}}
>
{trinityPersons.map((person) => (
<MenuItem key={person.value} value={person.value}>
{person.text}
</MenuItem>
)}
</TextField>
</FormControl>
);
Second way: with Select component
This way is the same of the first way, the difference here is that we create using a more explicit syntax provided by Material UI. Now is necessary to import the Select and InputLabel component.
import { InputLabel, Select } from "@material-ui/core";
Now we create our FormControl using the Select component:
return(
<FormControl>
<InputLabel htmlFor="trinity-select">
Choose one Person of trinity
</InputLabel>
<Select
id="trinity-select"
inputProps={{
inputRef: (ref) => {
if (!ref) return;
register({
name: "trinityPerson",
value: ref.value,
});
},
}}
>
{trinityPersons.map((person) => (
<MenuItem key={person.value} value={person.value}>
{person.text}
</MenuItem>
)}
</Select>
</FormControl>
);
Even using the the explicit Select component we suffer with the way that this component was created, as I said before.
there are some limitations in use some components that are created to work internally like a controlled
Third way: with Controller component
This is the way that React Hook Form recommend us. Is the best approach and work like a magic, the Controller component do all the work.
One more thing to import:
import { Controller } from "react-hook-form";
And do not forget do use the control of useForm hook
const { register, handleSubmit, control } = useForm();
The function of Controller component is to wrapper the controlled component and make it easier for work with them. It was created to work with Material UI 💖
And now we create our Select component inside the Controller.
return(
<FormControl>
<InputLabel htmlFor="trinity-select">
Choose one Person of trinity
</InputLabel>
<Controller
control={control}
name="trinityPerson"
as={
<Select id="trinity-select">
{trinityPersons.map((person) => (
<MenuItem key={person.value} value={person.value}>
{person.text}
</MenuItem>
)}
</Select>
}
/>
</FormControl>
);
Last way: creating a reusable component
And even exist the component way.
We create an wrapper component that abstract all the boilerplate and expose the use just like an simple Select component.
import FormControl from "@material-ui/core/FormControl";
import InputLabel from "@material-ui/core/InputLabel";
import Select from "@material-ui/core/Select";
import { Controller } from "react-hook-form";
const ReactHookFormSelect = ({
name,
label,
control,
defaultValue,
children,
...props
}) => {
const labelId = `${name}-label`;
return (
<FormControl {...props}>
<InputLabel id={labelId}>{label}</InputLabel>
<Controller
as={
<Select labelId={labelId} label={label}>
{children}
</Select>
}
name={name}
control={control}
defaultValue={defaultValue}
/>
</FormControl>
);
};
export default ReactHookFormSelect;
Now you can use it in your app like this:
<ReactHookFormSelect
id="trinity-select"
name="trinityPerson"
label="Choose one Person of trinity"
control={control}
>
{trinityPersons.map((person) => (
<MenuItem key={person.value} value={person.value}>
{person.text}
</MenuItem>
)}
</ReactHookFormSelect>
Useful links
https://github.com/react-hook-form/react-hook-form/issues/497
https://github.com/react-hook-form/react-hook-form/issues/380
https://stackoverflow.com/questions/63236951/how-to-use-material-ui-select-with-react-hook-form
Top comments (2)
when using the first way, i cant pre-fill any value from my hook.
always get this error:
"You have provided an out-of-range value
undefined
for the select (name="source") component.Consider providing a value that matches one of the available options or ''.
The available values are
602e4ed8ff9c0648deebbaa1
,602e4ed8ff9c0648deebbaa2
,602e4ed8ff9c0648deebbaa3
,602e4ed8ff9c0648deebbaa4
,602e4ed8ff9c0648deebbaa5
,602e4ed8ff9c0648deebbaa6
."my Select looks like this:
select
id="target"
name="target"
label="Target"
variant="outlined"
value={target.id}
ref={register}
onChange={(e) => setSystem(e.target.value, 'target')}>
{systems.map((system) => (
{system.name}
))}
however, if i use a classic html select and value it the same way, it works... any idea?
There's an error in your first option's code at
{trinityPersons.map((person) => (
{person.text}
)}
it should rather be
{trinityPersons.map((person) => (
{person.text}
)
)}