2

I am trying to open and close my Material UI <Menu /> by using React's setState hook. I used this sandbox code as an example. My implementation doesn't seem to close the menu however.

Opening it works fine (using setAnchorEl(event.currentTarget)), but closing it using setAnchorEl(null) does not somehow. The function handleClose() runs (the console.log() prints fine), but the value of anchorEl does not change. I checked this using the useEffect() hook, which prints null on initialization, the <li> element when opening and nothing when trying to close.

I already tried using let instead of const for the (set)anchorEl and isProfileMenuOpen definition, but this doesn't work. I also checked out this post, but my <MenuAppBar /> component is not being unmounted as far as I can see (it's rendered at the root unconditionally).

I must be overlooking some detail. Can anybody spot it below?

const {useState, useEffect} = React;
const { ClickAwayListener, Menu, MenuItem, IconButton } = MaterialUI;

function MenuAppBar(props) {
  const [anchorEl, setAnchorEl] = useState(null);
  const isProfileMenuOpen = Boolean(anchorEl);

  // For debugging purposes:
  useEffect(() => {
    console.log(anchorEl);
  }, [anchorEl]);

  const handleClose = () => {
    console.log("Closing menu"); // This prints, so function runs
    setAnchorEl(null); // This doesn't seem to set anchorEl to null
  };

  const handleProfileMenuOpen = (event) => {
    setAnchorEl(event.currentTarget); // This does work
  }

  return (
    <ClickAwayListener onClickAway={handleClose}>
      <MenuItem onClick={handleProfileMenuOpen}>
        <IconButton
          style={{ backgroundColor: "#F5B089" }}
        />
        <Menu
          anchorEl={anchorEl}
          keepMounted
          open={isProfileMenuOpen}
          onClose={handleClose}
        >
          <MenuItem onClick={handleClose}>Profile</MenuItem>
          <MenuItem onClick={handleClose}>My account</MenuItem>
          <MenuItem>Logout</MenuItem>
        </Menu>
      </MenuItem>
    </ClickAwayListener>
  );
}

ReactDOM.render(
  <MenuAppBar />,
  document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@material-ui/core@latest/umd/material-ui.development.js"></script>
<div id="react"></div>
samthecodingman
  • 23,122
  • 4
  • 30
  • 54
Axel Köhler
  • 911
  • 1
  • 8
  • 34
  • That's the downside of giving you a minimal reproducible example ;). Any idea what could generate noise setting state in react? – Axel Köhler May 10 '21 at 14:00
  • 2
    @T.J.Crowder I've thrown it in a snippet for you. Some bizarre behaviour indeed. – samthecodingman May 10 '21 at 14:28
  • @samthecodingman - Thanks, hopefully the OP appreciates it as well. It's just that `setAnchorEl` is getting called a second time, after the call setting `null`, putting the same `li` back in it. – T.J. Crowder May 10 '21 at 15:12

1 Answers1

4

What's happening is that after handleClose is called, handleProfileMenuOpen is called again. So you end up with two calls to setAnchorEl during the same click event, so React only re-renders the component once — with it set to what handleProfileMenuOpen set it to (or not at all if it was the same li, as it is in the Stack Snippet).

If you stop the event from propagating, it prevents the handleProfileMenuOpen call:

const handleClose = (event) => {
    console.log("Closing menu");
    setAnchorEl(null);
    event.stopPropagation(); // <=====
};

Live Example:

const {useState, useEffect} = React;
const { ClickAwayListener, Menu, MenuItem, IconButton } = MaterialUI;

function MenuAppBar(props) {
  const [anchorEl, setAnchorEl] = useState(null);
  const isProfileMenuOpen = Boolean(anchorEl);
  
  console.log(anchorEl);

  const handleClose = (event) => {
    console.log("Closing menu"); // This prints, so function runs
    setAnchorEl(null); // This doesn't seem to set anchorEl to null
    event.stopPropagation();
  };

  const handleProfileMenuOpen = (event) => {
    console.log("Opening menu");
    setAnchorEl(event.currentTarget); // This does work
  }

  return (
    <ClickAwayListener onClickAway={handleClose}>
      <MenuItem onClick={handleProfileMenuOpen}>
        <IconButton
          style={{ backgroundColor: "#F5B089" }}
        />
        <Menu
          anchorEl={anchorEl}
          keepMounted
          open={isProfileMenuOpen}
          onClose={handleClose}
        >
          <MenuItem onClick={handleClose}>Profile</MenuItem>
          <MenuItem onClick={handleClose}>My account</MenuItem>
          <MenuItem>Logout</MenuItem>
        </Menu>
      </MenuItem>
    </ClickAwayListener>
  );
}

ReactDOM.render(
  <MenuAppBar />,
  document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@material-ui/core@latest/umd/material-ui.development.js"></script>
<div id="react"></div>

Side note: If you want to see anchorEl changing as the component is rerendered, there's no need for a useEffect call, just log it at the top level of your component function (or put a breakpoint on the const [anchorEl ... line). Remember that the component function is called each time the component is rendered, so that will always show you the up-to-date version of anchorEl.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875