Search code examples
reactjsmaterial-uicontextmenu

MUI context menu in a list always has reference to the last item


I'm using Material UI for a context menu in a react app. I've made a min repro here:

Edit summer-silence-xzxwd3

export default function App() {
  const [contextMenu, setContextMenu] = React.useState(null);

  const myArray = ["item1", "item2", "item3"];

  const handleContextMenu = (event) => {
    event.preventDefault();
    setContextMenu(
      contextMenu === null
        ? {
            mouseX: event.clientX + 2,
            mouseY: event.clientY - 6
          }
        : null
    );
  };

  const editItem = (i) => {
    setContextMenu(null);
    alert(`Editing ${i}`);
  };

  return (
    <div>
      {myArray.map((i) => (
        <div key={i} onContextMenu={handleContextMenu}>
          <Card sx={{ backgroundColor: "gray", m: 2 }}>
            <CardContent>
              <Typography>{i}</Typography>
            </CardContent>
          </Card>
          <Menu
            open={contextMenu != null}
            onClose={() => setContextMenu(null)}
            anchorReference="anchorPosition"
            anchorPosition={
              contextMenu != null
                ? { top: contextMenu.mouseY, left: contextMenu.mouseX }
                : undefined
            }
          >
            <MenuItem onClick={() => editItem(i)}>Edit</MenuItem>
          </Menu>
        </div>
      ))}
    </div>
  );
}

Every time I right-click > "Edit", the alert always shows for the last item ("item3"). In my full app, I can also drag-drop to rearrange these cards and the behavior will still be whatever card is last in the list after the rearrange.

Not sure what I'm doing wrong here. I thought it might be a missing key issue, but I don't think that's the case as the only list item with siblings (the div) has a key. Any ideas?


Solution

  • If you inspect your page, you can see that all 3 menus open at the same time, and the last one always receives the click event.

    Here is how you can fix your code:

    import "./styles.css";
    import Card from "@mui/material/Card";
    import CardContent from "@mui/material/CardContent";
    import Typography from "@mui/material/Typography";
    import Menu from "@mui/material/Menu";
    import MenuItem from "@mui/material/MenuItem";
    import * as React from "react";
    
    export default function App() {
      const [contextMenu, setContextMenu] = React.useState(null);
    
      const myArray = ["item1", "item2", "item3"];
    
      const handleContextMenu = (item) => (event) => {
        event.preventDefault();
        setContextMenu(
          contextMenu === null
            ? {
                item,
                mouseX: event.clientX + 2,
                mouseY: event.clientY - 6
              }
            : null
        );
      };
    
      const editItem = () => {
        alert(`Editing ${contextMenu.item}`);
        setContextMenu(null);
      };
    
      return (
        <div>
          {myArray.map((i) => (
            <div key={i} onContextMenu={handleContextMenu(i)}>
              <Card sx={{ backgroundColor: "gray", m: 2 }}>
                <CardContent>
                  <Typography>{i}</Typography>
                </CardContent>
              </Card>
            </div>
          ))}
    
          <Menu
            open={contextMenu != null}
            onClose={() => setContextMenu(null)}
            anchorReference="anchorPosition"
            anchorPosition={
              contextMenu != null
                ? { top: contextMenu.mouseY, left: contextMenu.mouseX }
                : undefined
            }
          >
            <MenuItem onClick={editItem}>Edit</MenuItem>
          </Menu>
        </div>
      );
    }