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>
</>
}