Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

MUI Select custom MenuItem not working properly

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?

like image 271
dabljues Avatar asked Sep 10 '25 12:09

dabljues


1 Answers

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>
  );
}

Edit MUI Select custom MenuItem

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>
  );
}

Edit MUI Select custom MenuItem (forked)

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>
  );
}

Edit MUI Select custom MenuItem

like image 138
Ryan Cogswell Avatar answered Sep 13 '25 03:09

Ryan Cogswell