adverse-sapphire
adverse-sapphire•2w ago

How to make resource icons more flexible?

Hi there, I'm creating something like a CRM using Refine with Next.js and a Headless UI framework (I'm using Shadcn), so, I already have my resource items like this:
import { IResourceItem } from "@refinedev/core";
import { Building2, Home } from "lucide-react";

export const resources: IResourceItem[] = [
{
name: 'dashboard',
list: '/',
meta: {
label: 'Dashboard',
icon: <Home className="h-5 w-5" />,
},
},
{
name: 'companies',
list: '/companies',
show: '/companies/:id',
create: '/companies/new',
edit: '/companies/edit/:id',
meta: {
label: 'Companies',
icon: <Building2 className="h-5 w-5" />,
},
}
];
import { IResourceItem } from "@refinedev/core";
import { Building2, Home } from "lucide-react";

export const resources: IResourceItem[] = [
{
name: 'dashboard',
list: '/',
meta: {
label: 'Dashboard',
icon: <Home className="h-5 w-5" />,
},
},
{
name: 'companies',
list: '/companies',
show: '/companies/:id',
create: '/companies/new',
edit: '/companies/edit/:id',
meta: {
label: 'Companies',
icon: <Building2 className="h-5 w-5" />,
},
}
];
The main idea here is to have a Menu component and a Header (this one will contain a mobile version of the menu) component in both I use the useMenu hook to get the menu items but in each component I need to use different classNames for the icons. So the question is, how can I make the icon in the resource item accepts a function that returns a ReactNode (which is the expected type for the icon). I tried to create my custom type as follows:
interface CustomResourceItem extends IResourceItem {
meta: {
label: string;
icon: ((className?: string) => ReactNode) | ReactNode;
};
};
interface CustomResourceItem extends IResourceItem {
meta: {
label: string;
icon: ((className?: string) => ReactNode) | ReactNode;
};
};
But I get a TypeScript error and it's driving me crazy, so could anyone help me pls?
5 Replies
kapa.ai
kapa.ai•2w ago
Hi @SpiritHeiBai. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- To make the icon in the resource item more flexible and accept a function that returns a ReactNode, you can extend the IResourceItem interface to include a custom type for the icon property. This custom type can be a function that takes a className parameter and returns a ReactNode, or it can be a ReactNode directly. Here's how you can achieve this: 1. Extend the IResourceItem Interface: Define a new interface that extends IResourceItem and modifies the icon property in the meta object to accept either a ReactNode or a function that returns a ReactNode. 2. Update Your Resource Items: Use this new interface for your resource items and provide the icon property as a function where needed. Here is the updated code:
import { IResourceItem } from "@refinedev/core";
import { Building2, Home } from "lucide-react";
import { ReactNode } from "react";

// Extend the IResourceItem interface
interface CustomResourceItem extends IResourceItem {
meta: {
label: string;
icon: ((className?: string) => ReactNode) | ReactNode;
};
}

// Define your resources using the extended interface
export const resources: CustomResourceItem[] = [
{
name: 'dashboard',
list: '/',
meta: {
label: 'Dashboard',
icon: (className) => <Home className={`h-5 w-5 ${className}`} />,
},
},
{
name: 'companies',
list: '/companies',
show: '/companies/:id',
create: '/companies/new',
edit: '/companies/edit/:id',
meta: {
label: 'Companies',
icon: (className) => <Building2 className={`h-5 w-5 ${className}`} />,
},
}
];
import { IResourceItem } from "@refinedev/core";
import { Building2, Home } from "lucide-react";
import { ReactNode } from "react";

// Extend the IResourceItem interface
interface CustomResourceItem extends IResourceItem {
meta: {
label: string;
icon: ((className?: string) => ReactNode) | ReactNode;
};
}

// Define your resources using the extended interface
export const resources: CustomResourceItem[] = [
{
name: 'dashboard',
list: '/',
meta: {
label: 'Dashboard',
icon: (className) => <Home className={`h-5 w-5 ${className}`} />,
},
},
{
name: 'companies',
list: '/companies',
show: '/companies/:id',
create: '/companies/new',
edit: '/companies/edit/:id',
meta: {
label: 'Companies',
icon: (className) => <Building2 className={`h-5 w-5 ${className}`} />,
},
}
];
3. Use the icon Property in Your Components: When rendering the icons in your Menu and Header components, you can call the function with the desired className. Here is an example of how you might use it in a Menu component:
import React from "react";
import { useMenu } from "@refinedev/core";
import { NavLink } from "react-router-dom";
import { Button } from "../ui/button";

export const Menu = () => {
const { menuItems } = useMenu();

return (
<nav className="menu">
<ul className="flex items-center justify-start md:items-start">
{menuItems.map((item) => (
<li className="text-sm" key={item.key}>
<Button variant="ghost" className="flex gap-1">
<span>
{typeof item.icon === "function" ? item.icon("custom-class") : item.icon}
</span>
<NavLink className="" to={item.route ?? "/"}>
{item.label}
</NavLink>
</Button>
</li>
))}
</ul>
</nav>
);
};
import React from "react";
import { useMenu } from "@refinedev/core";
import { NavLink } from "react-router-dom";
import { Button } from "../ui/button";

export const Menu = () => {
const { menuItems } = useMenu();

return (
<nav className="menu">
<ul className="flex items-center justify-start md:items-start">
{menuItems.map((item) => (
<li className="text-sm" key={item.key}>
<Button variant="ghost" className="flex gap-1">
<span>
{typeof item.icon === "function" ? item.icon("custom-class") : item.icon}
</span>
<NavLink className="" to={item.route ?? "/"}>
{item.label}
</NavLink>
</Button>
</li>
))}
</ul>
</nav>
);
};
By following these steps, you can make the icon property in your resource items more flexible and allow it to accept a function that returns a ReactNode with custom classNames. For more details, you can refer to the Refine documentation (https://refine.dev/docs/core/refine-component/#icon) and the useResource hook documentation (https://refine.dev/docs/routing/hooks/use-resource/#return-value). ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai•2w ago
<@892137297519181935> kapa.ai is still learning and improving, please let us know how it did by reacting below
fair-rose
fair-rose•2w ago
Hello @SpiritHeiBai, unfortunately resource.meta.icon not designed to works as function thats why type is ReactNode . you can see following links how we use icon: breadcrumb: https://github.com/refinedev/refine/blob/master/packages/antd/src/components/breadcrumb/index.tsx#L56C15-L56C29 sider: https://github.com/refinedev/refine/blob/master/packages/antd/src/components/themedLayoutV2/sider/index.tsx#L105 But it's a great idea. I will talk with the core team to implement something like this. but while we're waiting for this feature, you can make these improvements: just can add customIcon field to meta and you can make your implementation of <Sider>
export const resources: IResourceItem[] = [
{
name: 'dashboard',
list: '/',
meta: {
label: 'Dashboard',
icon: <Building2 className="h-5 w-5" />
customIcon: (className) => <Building2 className={`h-5 w-5 ${className}`} />,
},
},

];

export const Menu = () => {
const { menuItems } = useMenu();

return (
<nav className="menu">
<ul className="flex items-center justify-start md:items-start">
{menuItems.map((item) => (
<li className="text-sm" key={item.key}>
<Button variant="ghost" className="flex gap-1">
<span>
{item.meta?.customIcon("custom-class") && item.icon}
</span>
<NavLink className="" to={item.route ?? "/"}>
{item.label}
</NavLink>
</Button>
</li>
))}
</ul>
</nav>
);
};
export const resources: IResourceItem[] = [
{
name: 'dashboard',
list: '/',
meta: {
label: 'Dashboard',
icon: <Building2 className="h-5 w-5" />
customIcon: (className) => <Building2 className={`h-5 w-5 ${className}`} />,
},
},

];

export const Menu = () => {
const { menuItems } = useMenu();

return (
<nav className="menu">
<ul className="flex items-center justify-start md:items-start">
{menuItems.map((item) => (
<li className="text-sm" key={item.key}>
<Button variant="ghost" className="flex gap-1">
<span>
{item.meta?.customIcon("custom-class") && item.icon}
</span>
<NavLink className="" to={item.route ?? "/"}>
{item.label}
</NavLink>
</Button>
</li>
))}
</ul>
</nav>
);
};
adverse-sapphire
adverse-sapphire•2w ago
Thanks so much @Alican Erdurmaz I do really appreciate it. Unfortunately, I already did that as well and I get the following error, is there anything I need to change/update to make it work?
No description
adverse-sapphire
adverse-sapphire•2w ago
Ok, just for the record, I think I found a way to do it. Maybe it's not the cleanest way to achieve it but it works. So here is my solution and hopefully works for some else. I had to create a wrapper function that actually accepts the icon and some additional props to pass to it and returns a new element with those props, so here's the function:
"use client";

import {
cloneElement,
HTMLAttributes,
isValidElement,
ReactNode,
} from 'react';

type CustomIconWithPropsType<T extends ReactNode> = {
icon: T;
props: HTMLAttributes<HTMLElement> & Record<string, any>;
};

const CustomIconWithProps = <T extends ReactNode>({
icon,
props,
}: CustomIconWithPropsType<T>) => {
if (isValidElement) {
return cloneElement(icon, props);
}

return icon;
};
"use client";

import {
cloneElement,
HTMLAttributes,
isValidElement,
ReactNode,
} from 'react';

type CustomIconWithPropsType<T extends ReactNode> = {
icon: T;
props: HTMLAttributes<HTMLElement> & Record<string, any>;
};

const CustomIconWithProps = <T extends ReactNode>({
icon,
props,
}: CustomIconWithPropsType<T>) => {
if (isValidElement) {
return cloneElement(icon, props);
}

return icon;
};
At this point idk exactly the performance issues that might cause this, but still work, if there is a better way to do it or there's an improvement you want to make to the code, you're more than welcome to add it and share it 🙂 So, to use it I think is pretty clear but if not here is an example:
export const resources: IResourceItem[] = [
{
name: 'dashboard',
list: '/',
meta: {
label: 'Dashboard',
icon: <Building2 />,
},
},
];

export const Menu = () => {
const { menuItems } = useMenu();

return (
<nav className="menu">
<ul className="flex items-center justify-start md:items-start">
{menuItems.map((item) => {
const customIcon = CustomIconWithProps({ icon: item.icon, props: { className: 'h-4 w-4' } });

return(
<li className="text-sm" key={item.key}>
<Button variant="ghost" className="flex gap-1">
<span>
{customIcon}
</span>
<NavLink className="" to={item.route ?? "/"}>
{item.label}
</NavLink>
</Button>
</li>
)})}
</ul>
</nav>
);
};
export const resources: IResourceItem[] = [
{
name: 'dashboard',
list: '/',
meta: {
label: 'Dashboard',
icon: <Building2 />,
},
},
];

export const Menu = () => {
const { menuItems } = useMenu();

return (
<nav className="menu">
<ul className="flex items-center justify-start md:items-start">
{menuItems.map((item) => {
const customIcon = CustomIconWithProps({ icon: item.icon, props: { className: 'h-4 w-4' } });

return(
<li className="text-sm" key={item.key}>
<Button variant="ghost" className="flex gap-1">
<span>
{customIcon}
</span>
<NavLink className="" to={item.route ?? "/"}>
{item.label}
</NavLink>
</Button>
</li>
)})}
</ul>
</nav>
);
};