I have a problem regarding MUI's MenuItem
when combined with Select
and rendering it in a separate component.
Here's the codesandbox
Basically, I have something like this:
import { Select } from "@material-ui/core";
import CustomMenuItem from "./CustomMenuItem";
import React from "react";
export default function App() {
const userIds = [1, 2, 3];
return (
<Select
id="user"
name="User"
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
alert(event.target.value as number);
}}
>
{userIds.map((userId) => (
<CustomMenuItem key={userId} userId={userId} />
))}
</Select>
);
}
And this is the custom item:
import { MenuItem, Typography } from "@material-ui/core";
import React from "react";
interface CustomMenuItemProps {
userId: number;
}
const CustomMenuItem = React.forwardRef<HTMLLIElement, CustomMenuItemProps>(
(props: CustomMenuItemProps, ref) => {
const { userId, ...rest } = props;
return (
<MenuItem value={userId} {...rest} ref={ref}>
<Typography>{userId}</Typography>
</MenuItem>
);
}
);
export default CustomMenuItem;
At first, I've done this without any refs, but this gave me an error in the console (Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
), so after googling a while, I found out that I have to pass this ref. I also pass the ...rest
of the props, as I understand that the MenuItem
needs them.
Expected behavior: when I click on the MenuItem
, it gets selected in the Select
component.
Actual behavior: nothing happens.
The thing is, I made the CustomMenuItem
to make it reusable. But before that, I had a simple function like: renderItem
which I used both in Select.renderValue
and in userIds.map
and it had the same code as CustomMenuItem
- it returned the same JSX tree. And it worked then, but it doesn't work now, for some reason. So if I would do:
<Select
id="user"
name="User"
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
alert(event.target.value as number);
}}
>
{userIds.map((userId) => (
<MenuItem key={userId} value={userId}>
<Typography>{userId}</Typography>
</MenuItem>
))}
</Select>
it simply works :(
Am I missing something here?
There are a few implementation details of Select
that get in the way of trying to customize MenuItem
in this way.
Select
uses the value prop of its immediate children. The immediate children of the Select
in your case are CustomMenuItem
elements which only have a userId
prop -- not a value
prop; so Select
finds undefined
as the new value when you click on one of your custom menu items.
You can fix this aspect by duplicating your userId
prop as a value
prop:
import { Select } from "@material-ui/core";
import CustomMenuItem from "./CustomMenuItem";
import React from "react";
export default function App() {
const userIds = [1, 2, 3];
const [value, setValue] = React.useState(1);
console.log("value", value);
return (
<Select
id="user"
name="User"
value={value}
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
setValue(event.target.value as number);
}}
>
{userIds.map((userId) => (
<CustomMenuItem key={userId} value={userId} userId={userId} />
))}
</Select>
);
}
This then successfully changes the value of the Select
if you look at the console logs. The new value is not successfully displayed due to a separate problem I'll explain later.
You may think "then I can just use the value
prop instead of the userId
prop rather than having both", but the value
prop won't actually reach your custom component. Select
uses React.cloneElement
to change the value prop to undefined and instead puts it in data-value
to avoid a value
prop being specified in the final html (which wouldn't be a valid attribute for the html element that gets rendered).
In my sandbox above, you'll notice that when you select a value, the new value is not successfully displayed as the selected value. This is because Select
uses the children prop of the selected child as the display value unless you specify the renderValue prop. The children
prop of the CustomMenuItem
element is undefined.
You can fix this by either using the renderValue
prop on the Select
or by specifying the userId
yet again as a child:
import { Select } from "@material-ui/core";
import CustomMenuItem from "./CustomMenuItem";
import React from "react";
export default function App() {
const userIds = [1, 2, 3];
const [value, setValue] = React.useState(1);
console.log("value", value);
return (
<Select
id="user"
name="User"
value={value}
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
setValue(event.target.value as number);
}}
>
{userIds.map((userId) => (
<CustomMenuItem key={userId} value={userId} userId={userId}>
{userId}
</CustomMenuItem>
))}
</Select>
);
}
This works, but also removes all of the value the custom menu item component was trying to provide. I think the simplest way to achieve this (while still working well with the Material-UI Select
design) is to put the reusable code in a function for rendering the menu items rather than making a custom menu item component:
import { Select } from "@material-ui/core";
import React from "react";
import { MenuItem, Typography } from "@material-ui/core";
const renderMenuItem = (value: number) => {
return (
<MenuItem key={value} value={value}>
<Typography>{value}</Typography>
</MenuItem>
);
};
export default function App() {
const userIds = [1, 2, 3];
const [value, setValue] = React.useState(1);
console.log("value", value);
return (
<Select
id="user"
name="User"
value={value}
onChange={(event: React.ChangeEvent<{ value: unknown }>) => {
setValue(event.target.value as number);
}}
>
{userIds.map(renderMenuItem)}
</Select>
);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With