I am building an admin panel for a shop and using HeadlessUI to build it with <Tab>
instead of pages. With this approach tabs work correctly but then I tried to add Roles for administrators, so I created an object with allowedTabs:
export const allowedTabs = {
admin: {
shop: [EShopTab.ORDERS, EShopTab.PRODUCTS, EShopTab.STATISTICS],
system: [ESystemTab.USERS, ESystemTab.ADMINS, ESystemTab.SETTINGS],
},
moderator: {
shop: [EShopTab.ORDERS, EShopTab.PRODUCTS],
system: [ESystemTab.SETTINGS],
},
accountant: {
shop: [EShopTab.STATISTICS],
system: [ESystemTab.SETTINGS],
},
analyst: {
shop: [EShopTab.STATISTICS],
system: [ESystemTab.SETTINGS],
},
};
after that I'm mapping through them in tabs
and tab.panels
:
SidebarTabs.tsx:
export const SidebarTabs = () => {
return (
<Tab.List className={s.tabs}>
{Object.keys(tabs).map((key, index) => (
<SidebarSection sectionKey={key as ESidebarSectionKey} key={index} />
))}
</Tab.List>
);
};
SidebarSection.tsx:
export const SidebarSection = ({ sectionKey }: TProps) => {
const { admin } = useSelector((state: RootState) => state.admins);
const t = useTranslations('Sidebar');
return (
<section className={s.sidebarSection}>
<h1 className={s.title}>{t(sectionKey)}</h1>
<section className={s.sectionLinks}>
{allowedTabs &&
allowedTabs[admin.role] &&
Object.values(allowedTabs[admin.role][sectionKey]).map((key, index) => (
<SidebarTab key={index} label={key} />
))}
</section>
</section>
);
};
SidebarTab.tsx:
export const SidebarTab = ({ label }: TProps) => {
const t = useTranslations('Sidebar.Tabs');
return (
<Tab as={Fragment}>
{({ selected }) => (
<button
className={cn(s.tab, {
[s.active]: selected,
})}
>
{t(label)}
</button>
)}
</Tab>
);
};
After I have tabs I am rendering their panels in another component. As I cannot just render 4 tabs and 8 panels (because 1st tab will relate to the wrong panel) I am also mapping through allowedTabs:
Workspace.tsx:
export const Workspace = () => {
const { admin } = useSelector((state: RootState) => state.admins);
return (
<section className={s.workspace}>
<Tab.Panels>
{Object.values(allowedTabs[admin.role].shop).map((key, index) => (
<Tab.Panel key={index}>{panelByAllowedTab[key]}</Tab.Panel>
))}
{Object.values(allowedTabs[admin.role].system).map((key, index) => (
<Tab.Panel key={index}>{panelByAllowedTab[key]}</Tab.Panel>
))}
</Tab.Panels>
</section>
);
};
Rendering panel (Considering @Konrad comment changed all panels to OrdersTab
, problem remains):
tabs.ts:
export const panelByAllowedTab = {
[EShopTab.ORDERS]: OrdersTab,
[EShopTab.PRODUCTS]: OrdersTab,
[EShopTab.STATISTICS]: OrdersTab,
[ESystemTab.USERS]: OrdersTab,
[ESystemTab.ADMINS]: OrdersTab,
[ESystemTab.SETTINGS]: OrdersTab,
};
Adding OrdersTab:
export const OrdersTab = () => {
const dispatch = useDispatch();
const { orders } = useSelector((state: RootState) => state.orders);
const { products } = useSelector((state: RootState) => state.products);
const t = useTranslations('Headers.Orders');
useEffect(() => {
dispatch(fetchProducts());
dispatch(fetchOrders());
}, []);
return (
<TabLayout title="Orders">
<section className={s.content}>
<div className={s.header}>
<p className={s.orderNumber}>{t('orderNumber')}</p>
<p className={s.orderDate}>{t('date')}</p>
<p className={s.status}>{t('status')}</p>
<p className={s.orderTotal}>{t('total')}</p>
<p className={s.orderDelivery}>{t('delivery')}</p>
<p className={s.orderAddress}>{t('address')}</p>
<p className={s.orderPayment}>{t('payment')}</p>
</div>
<Button
onClick={() => {
dispatch(
createOrder({
status: EStatus.NEW,
totalprice: 0,
createdat: new Date().toISOString(),
deliverytype: EDelivery.PICKUP,
address: 'St',
paymenttype: EPayment.CARD,
contactphone: '37721732',
paymentstatus: EPaymentStatus.NOT_PAID,
filters: [
{
id: 'd8171941-f4f0-40b7-8642-19bc206d89ec',
filters: [
{
label: 'Weight',
value: '1',
extraprice: 0,
},
{
label: 'Color',
value: '#381232',
extraprice: 0,
},
],
},
],
ordernumber: `${orders.length + 1}`,
name: 'Name',
products: ['d8171941-f4f0-40b7-8642-19bc206d89ec'].map(id => {
return products.find(
product => product.id === id,
) as TProductDTO;
}),
id: crypto.randomUUID() as UUID,
}),
);
}}
>
Add order
</Button>
{orders &&
orders.length > 0 &&
orders.map(order => <OrdersCard key={order.id} id={order.id} />)}
</section>
</TabLayout>
);
};
and TabLayout:
export const TabLayout = ({ children, title }: TProps) => {
return (
<section className={s.tab}>
<h1 className={s.title}>{title}</h1>
<Divider />
{children}
</section>
);
};
With this tabs and panels rendering when I try to click on another panel I receive:
Unhandled Runtime Error
Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
A few things happened here:
I'm not exactly sure about that but it looks like Tab.Panel
can accept a function as child (Tabs
can for sure read more)
OrdersTab
is treated as a function if not used as <OrdersTab />
So OrdersTab
was probably called as a function a different number of times which caused useDispatch
to be called
Either replace [EShopTab.ORDERS]: OrdersTab
with [EShopTab.ORDERS]: <OrdersTab />
Or define Workspace
component like this:
export const Workspace = () => {
const { admin } = useSelector((state: RootState) => state.admins);
// any name, but must be capitalized
const Component = panelByAllowedTab[key]
return (
<section className={s.workspace}>
<Tab.Panels>
{Object.values(allowedTabs[admin.role].shop).map((key, index) => (
<Tab.Panel key={index}><Component /></Tab.Panel>
))}
{Object.values(allowedTabs[admin.role].system).map((key, index) => (
<Tab.Panel key={index}><Component /></Tab.Panel>
))}
</Tab.Panels>
</section>
);
};