Add state as property on each item of an array of object

I’m developing an React app that have different user types. Each user has specific items on it’s menu. An item may be a link or a submenu. I want to render buttons for link items and collapses (of react-bootstrap) for submenus. But, to have the Collapse to work properly, I need to add a boolean state on each submenu item.

All menus are separated on a file, this way:

// /utils/Menus.js

export const AdminMenu = [
    { label: "Dashboard", href: "/dashboard" },
    { label: "Users", href: "#", subitems: [
        { label: "All Users", href: "/users" },
        { label: "New User", href: "/users/new" }
    ] },
    { label: "Products", href: "#", subitems: [
        { label: "All Products", href: "/products" },
        { label: "New Product", href: "/products/new" }
    ] },
    { label: "Clients", href: "#", subitems: [
        { label: "All Clients", href: "/clients" },
        { label: "New Client", href: "/clients/new" }
    ] },
    { label: "Orders", href: "/orders", subitems: [
        { label: "All Orders", href: "/orders" },
        { label: "Cancelled", href: "/orders/cancelled" },
        { label: "Latest", href: "/orders/latest" }
    ] },
    { label: "My Profile", href: "/my-profile" },
    { label: "Logout", href: "/logout" }
];

export const ClientMenu = [
    { label: "Dashboard", href: "/dashboard" },
    { label: "My Orders", href: "/orders" },
    { label: "My Profile", href: "/my-profile" },
    { label: "Logout", href: "/logout" }
];

export const ManagerMenu = [
    { label: "Dashboard", href: "/dashboard" },
    { label: "Products", href: "#", subitems: [
        { label: "All Products", href: "/products" },
        { label: "New Product", href: "/products/new" }
    ] },
    { label: "Orders", href: "/orders", subitems: [
        { label: "All Orders", href: "/orders" },
        { label: "Cancelled", href: "/orders/cancelled" },
        { label: "Latest", href: "/orders/latest" }
    ] },
    { label: "My Profile", href: "/my-profile" },
    { label: "Logout", href: "/logout" }
];

And I have a sidebar component that renders the menu according on logged user profile. On this component, I want to add an open state property on each menu item that has the subitems property. This way:

// /components/Sidenav.js

[...]
const [userMenu, setUserMenu] = useState([]);

useEffect(() => {
    let menu = [];

    switch(user.role){
        case "admin":
            menu = [...AdminMenu];
            break;
        case "client":
            menu = [...ClientMenu];
            break;
        case "manager":
            menu = [...ManagerMenu];
            break;
    }

    menu = menu.map((item) => {
        if(item.subitems){
            item = {...item, open: useState(false)}
        }
        return item;
    });

    setUserMenu(menu);
}, []);

return (
    <ul>
        {userMenu.map((item, i) => (
            <li key={i}>
                { item.subitems ? (
                <>
                    <button onClick={() => { item.open[1](!item.open[0]) }}>{item.label}</button>
                    <Collapse in={item.open[0]}>\
                        <ul>
                            { item.subitems.map((subitem, j) => (
                                <li key={j}>
                                    <Link href={subitem.href}>{subitem.label}</Link>
                                </li>
                            )) }
                        </ul>
                    </Collapse>
                </>
                ) : (
                    <Link href={item.href}>{item.label}</Link>
                ) }
            </li>
        ))}
    </ul>
);

So, the approach is to try to dynamically add the open state on each item that has subitems. And, on render, use item.open[0] (the state) and item.open[1] (the set state function) to toggle the Collapse’s in.

The problem is: React is throwing an error saying I can’t use useState outside a function component. But when I add the open property on items, I’m inside an useEffect that is on a function component. So where’s the problem?

I’ve tested declaring the menus on the component as constants, and directly attribuing the open state where necessary, and it’s worked as expected.

// /components/Sidenav.js

[...]
const [userMenu, setUserMenu] = useState([]);

const AdminMenu = [
    { label: "Dashboard", href: "/dashboard" },
    { label: "Users", href: "#", open: useState(false), subitems: [
        { label: "All Users", href: "/users" },
        { label: "New User", href: "/users/new" }
    ] },
    { label: "Products", href: "#", open: useState(false), subitems: [
        { label: "All Products", href: "/products" },
        { label: "New Product", href: "/products/new" }
    ] },
    { label: "Clients", href: "#", open: useState(false), subitems: [
        { label: "All Clients", href: "/clients" },
        { label: "New Client", href: "/clients/new" }
    ] },
    { label: "Orders", href: "/orders", open: useState(false), subitems: [
        { label: "All Orders", href: "/orders" },
        { label: "Cancelled", href: "/orders/cancelled" },
        { label: "Latest", href: "/orders/latest" }
    ] },
    { label: "My Profile", href: "/my-profile" },
    { label: "Logout", href: "/logout" }
];

const ClientMenu = [
    { label: "Dashboard", href: "/dashboard" },
    { label: "My Orders", href: "/orders" },
    { label: "My Profile", href: "/my-profile" },
    { label: "Logout", href: "/logout" }
];

const ManagerMenu = [
    { label: "Dashboard", href: "/dashboard" },
    { label: "Products", href: "#", open: useState(false), subitems: [
        { label: "All Products", href: "/products" },
        { label: "New Product", href: "/products/new" }
    ] },
    { label: "Orders", href: "/orders", open: useState(false), subitems: [
        { label: "All Orders", href: "/orders" },
        { label: "Cancelled", href: "/orders/cancelled" },
        { label: "Latest", href: "/orders/latest" }
    ] },
    { label: "My Profile", href: "/my-profile" },
    { label: "Logout", href: "/logout" }
];

useEffect(() => {
    switch(user.role){
        case "admin":
            setUserMenu(AdminMenu);
            break;
        case "client":
            setUserMenu(ClientMenu);
            break;
        case "manager":
            setUserMenu(ManagerMenu);
            break;
    }
}, []);

return (
    [...]
);

But this makes me to repeat the menu constants everywhere I need to show up a user menu. It isn’t the best way to do that.

So, how can I add the open state on menu items, to dynamically render links or collapses?

>Solution :

I think it’s simpler to make it a component with the state such that(i’m freestyling below so the code might not work)

const CollapseMenu = ({
  item,
}) => {
  const [isOpen, setIsOpen] = useState(false)

  return <>
          <button onClick={() => { setIsOpen(!isOpen) }}>{item.label}</button>
          <Collapse in={isOpen}>\
              <ul>
                  { item.subitems.map((subitem, j) => (
                      <li key={j}>
                          <Link href={subitem.href}>{subitem.label}</Link>
                      </li>
                  )) }
              </ul>
          </Collapse>
  </>
}

Leave a Reply