Home > Software engineering >  Issue in removing Grandchild in a recursive component
Issue in removing Grandchild in a recursive component

Time:09-14

What I have been trying to achieve?

Create a nested context menu that is driven by a config.


Where am I stuck:

Sub menus are rendering correctly, but if there is more than 2 level, the change in root level only affects its sub-menu and not its entire tree

Here is the sandbox link for you to check.


Steps to reproduce:

  1. On load, a menu is displayed (say menu)
  2. Click on File, it will open its sub menu (say sub-menu1).
  3. Click on Open in the sub-menu1, again another sub menu (say sub-menu2) is open.
  4. Now when you click on Edit in menu, sub-menu1 disappears but not sub-menu2

I think, I know the problem. sub-menu2 is not refreshing because props or state is not changed. To hide it, we will need to trickle down some prop but can't think of elegant way to do it without state management system.

CodePudding user response:

You'll have a better time if the ContextMenu component is responsible for state management and recursion is flattened into iteration.

function ContextItem({ item, onClick }) {
  return (
      <div className="menu-item" onClick={() => onClick(item)}>
        <p className="menu-title">{item.title}</p>
        {item.children && item.children.length > 0 ? <i className="right-icon">{">"}</i> : null}
      </div>
  );
}

function MenuList({ list, onClick }) {
  return (
    <div className="menu-container">
      {list.map((listItem) => (
        <ContextItem item={listItem} key={listItem.title} onClick={onClick} />
      ))}
    </div>
  );
}

const ContextMenu = ({ list }) => {
  const [openSubmenus, setOpenSubmenus] = React.useState([]);
  const clickHandler = React.useCallback((item, level) => {
    if (item.children && item.children.length) {
      setOpenSubmenus((oldItems) => {
        return [...oldItems.slice(0, level), item.children];
      });
    } else {
      setOpenSubmenus([]); // item selected, clear submenus
      alert(item.title);
    }
  }, []);
  const menus = [list, ...openSubmenus];
  return (
    <div className="menu">
      {menus.map((menu, level) => (
        <MenuList
          key={level}
          list={menu}
          level={level}
          onClick={(item) => clickHandler(item, level)}
        />
      ))}
    </div>
  );
};
const menuList = [{
  title: "File",
  children: [{
    title: "Close",
    children: [],
    action: "fileClose",
  }, {
    title: "Open",
    children: [{
      title: "A:\\",
      children: [],
      action: "",
    }, {
      title: "C:\\",
      children: [],
      action: "",
    }, {
      title: "\\",
      children: [],
      action: "",
    }],
    action: "",
  }, {
    title: "Find",
    children: [{
      title: "here",
      children: [],
    }, {
      title: "elsewhere",
      children: [],
    }],
    action: "",
  }, {
    title: "Backup",
    children: [],
    action: "backup",
  }],
  action: "",
}, {
  title: "Edit",
  children: [],
  action: "edit",
}];

function App() {
  return <ContextMenu list={menuList} />;
}

ReactDOM.render(<App />, document.getElementById("root"));
.menu {
  display: flex;
  flex-direction: row;
}

.menu-container {
  display: flex;
  flex-direction: column;
  background-color: #eee;
  border: 1px solid gray;
  border-radius: 4px;
}

.menu-item {
  display: flex;
  flex-direction: row;
  margin: 2px;
  max-width: 200px;
  line-height: 30px;
  padding: 5px 10px;
}

.menu-title {
  min-width: 80px;
  height: 30px;
  flex-grow: 1;
  margin: 0;
  vertical-align: middle;
}
.menu-title.active {
  background-color: blue;
  color: white;
}
.right-icon {
  width: 25px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.0.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

CodePudding user response:

You could use the label as the key to reset the ContextMenu state when selectedItem change (assuming the label is unique at a given depth, but it seems reasonable, otherwise you could add unique ids).

export const ContextMenu = ({list}) => {
    const [selectedItem, setSelectedItem] = useState();

    return (
        <div className="menu">
            <div className="menu-container">
                {list.map((listItem) => {
                    return (
                        <ContextItem
                            item={listItem}
                            key={listItem.title}
                            onClick={setSelectedItem}
                        />
                    );
                })}
            </div>
            {selectedItem?.children.length > 0 && <ContextMenu
                key={selectedItem.title}
                list={selectedItem.children}/>}
        </div>
    );
};
  • Related