Open tsnery opened 6 months ago
You can render an svg element pointing to tabler-sprite-nostroke.svg#ICON_NAME
whether its in node_modules or wherever
type Props = {
icon: string;
color?: string;
size?: number;
};
export function Icon({ icon, color = "inherit", size = 18 }: Props) {
return (
<svg
style={{
stroke: "currentColor",
strokeWidth: 1.75,
strokeLinecap: "round",
strokeLinejoin: "round",
fill: "none",
color: color,
width: `${size}px`,
height: `${size}px`
}}
>
<use href={"path/to/node_modules/@tabler/icons/tabler-sprite-nostroke.svg#tabler-" + icon} />
</svg>
);
}
This is how I was doing it at first but I had issues with Electron loading in the entire sprite sheet for each icon in production mode (worked fine in dev mode), so I also have the Tabler Icons Webfont package and use this instead:
import "@tabler/icons-webfont/tabler-icons.min.css";
type Props = {
icon: string;
color?: string;
size?: number;
};
export function Icon({ icon, color = "inherit", size = 18 }: Props) {
return (
<i
className={`ti ti-${icon}`}
style={{
fontSize: `${size}px`,
color: color
}}
/>
);
}
Have you tried rendering a list of all these icons?
I support the addition of this capability :+1:
I'm currently working on a component that requires the ability to accept an icon name as a prop and dynamically render it inside. Having this feature would greatly enhance the flexibility of the component.
Additionally, it would be beneficial to have an enum or type that provides a comprehensive list of all available icons. This way, developers can easily reference and choose from a standardized set of icons when using the component.
Here is my attempt:
(Optional: get a list of all available icons) Then Display the icon by its name.
import { icons } from "@tabler/icons-react";
import { useState } from "react";
import dynamic from "next/dynamic";
export default function Home() {
const [icon, setIcon] = useState<string>("IconAbc");
return (
<main className={`flex min-h-screen flex-col items-center p-24 gap-4`}>
<DynamicIcon icon={icon} />
{icon}
<div className="grid grid-cols-12 h-96 overflow-scroll">
{Object.entries(icons).map(([key, Value]) => (
<button
className="p-1 hover:bg-red-500"
key={key}
onClick={() => setIcon(key)}
title={key}
>
<Value />
</button>
))}
</div>
</main>
);
}
const DynamicIcon = ({ icon }: { icon?: string }) => {
if (!icon) return null;
const Icon = dynamic(
() => import(`@tabler/icons-react/dist/esm/icons/${icon}.mjs`),
{
loading: () => <p>Loading...</p>,
}
);
return <Icon />;
};
The problem with my previously mentioned solution is that Webpack generates an entry map with all kinds of icons when it is created, which is added when the page is first loaded and is more than 50kb.
Looks like this:
"./IconGalaxy.mjs":[183111,183111],"./IconGardenCart.mjs":[643864,643864],"./IconGardenCartOff.mjs":[49835,49835],"./IconGasStation.mjs":[913266,913266],"./IconGasStationOff.mjs":[695510,695510],"./IconGauge.mjs":[890866,890866],"./IconGaugeFilled.mjs":[161194,161194],"./IconGaugeOff.mjs":[622710,622710],"./IconGavel.mjs":[103125,103125],"./IconGenderAgender.mjs":[467566,467566],"./IconGenderAndrogyne.mjs":[951129,951129],"./IconGenderBigender.mjs":[489974,489974],"./IconGenderDemiboy.mjs":[971931,971931],"./IconGenderDemigirl.mjs":[158563,158563],"./IconGenderEpicene.mjs":[750659,750659],"./IconGenderFemale.mjs":[883878,883878],"./IconGenderFemme.mjs":[527029,527029],"./IconGenderGenderfluid.mjs":[226052,226052],"./IconGenderGenderless.mjs":[640555,640555],"./IconGenderGenderqueer.mjs":[618596,618596],"
The icons are bundled in individual chunks, which I think is correct behavior.
Does anyone know how to optimize this to avoid the 50kb
first-load, or does anyone know a better implementation?
I found out that if you create an object that contains all icons -> dynamic import
and then dynamically import it, the 50kb
chunk is split on first load.
import { iconsList } from "@tabler/icons-react";
import set from "lodash/set";
import type { FC } from "react";
import { iconToPascalCase } from "./utils";
export const dynamicIconImports: Record<
(typeof iconsList)["default"][number],
() => Promise<FC>
> = iconsList.default.reduce((prev, icon) => {
const pascalIconName = iconToPascalCase(icon);
set(
prev,
pascalIconName,
() => import(`@tabler/icons-react/dist/esm/icons/Icon${pascalIconName}.mjs`)
);
return prev;
}, {});
Then import into the dynamicIcon component.
import { IconFileUnknown, IconLoader } from "@tabler/icons-react";
import dynamic from "next/dynamic";
const LoadingIcon = () => (
<IconLoader className="animate-pulse" aria-label="Icon wird geladen..." />
);
export const DynamicIcon = (icon: string)=> {
const Icon = dynamic(
async () => {
const importMap = (await import(`./dynamicImports`)).dynamicIconImports;
return importMap[icon]?.().catch(() => ({
default: () => <IconFileUnknown className="opacity-50" />,
}));
},
{
loading: LoadingIcon,
}
);
return Icon;
};
Now it's a separate chunk. Not ideal, but still better than having 50kb
in the first load.
If this object containing the dynamic imports were provided by @tabler/icons-react
, webpack could statically analyze it and store potential network requests and data.
This also eliminates the need to convert the iconsList to pascalCase on the end user's side.
const dynamicIconImports = {
"a-b-2": () => import(`@tabler/icons-react/dist/esm/icons/IconAB2.mjs`),
"a-b-off": () => import(`@tabler/icons-react/dist/esm/icons/IconABOff.mjs`),
...
}
I will try to make a PR later.
Update 1
The problem with my previously mentioned solution is that Webpack generates an entry map with all kinds of icons when it is created, which is added when the page is first loaded and is more than 50kb.
Looks like this:
"./IconGalaxy.mjs":[183111,183111],"./IconGardenCart.mjs":[643864,643864],"./IconGardenCartOff.mjs":[49835,49835],"./IconGasStation.mjs":[913266,913266],"./IconGasStationOff.mjs":[695510,695510],"./IconGauge.mjs":[890866,890866],"./IconGaugeFilled.mjs":[161194,161194],"./IconGaugeOff.mjs":[622710,622710],"./IconGavel.mjs":[103125,103125],"./IconGenderAgender.mjs":[467566,467566],"./IconGenderAndrogyne.mjs":[951129,951129],"./IconGenderBigender.mjs":[489974,489974],"./IconGenderDemiboy.mjs":[971931,971931],"./IconGenderDemigirl.mjs":[158563,158563],"./IconGenderEpicene.mjs":[750659,750659],"./IconGenderFemale.mjs":[883878,883878],"./IconGenderFemme.mjs":[527029,527029],"./IconGenderGenderfluid.mjs":[226052,226052],"./IconGenderGenderless.mjs":[640555,640555],"./IconGenderGenderqueer.mjs":[618596,618596],"
The icons are bundled in individual chunks, which I think is correct behavior.
Does anyone know how to optimize this to avoid the
50kb
first-load, or does anyone know a better implementation?Update 2
I found out that if you create an object that contains all
icons -> dynamic import
and then dynamically import it, the50kb
chunk is split on first load.dynamicImports.ts
import { iconsList } from "@tabler/icons-react"; import set from "lodash/set"; import type { FC } from "react"; import { iconToPascalCase } from "./utils"; export const dynamicIconImports: Record< (typeof iconsList)["default"][number], () => Promise<FC> > = iconsList.default.reduce((prev, icon) => { const pascalIconName = iconToPascalCase(icon); set( prev, pascalIconName, () => import(`@tabler/icons-react/dist/esm/icons/Icon${pascalIconName}.mjs`) ); return prev; }, {});
Then import into the dynamicIcon component.
dynamicIcon.tsx
import { IconFileUnknown, IconLoader } from "@tabler/icons-react"; import dynamic from "next/dynamic"; const LoadingIcon = () => ( <IconLoader className="animate-pulse" aria-label="Icon wird geladen..." /> ); export const DynamicIcon = (icon: string)=> { const Icon = dynamic( async () => { const importMap = (await import(`./dynamicImports`)).dynamicIconImports; return importMap[icon]?.().catch(() => ({ default: () => <IconFileUnknown className="opacity-50" />, })); }, { loading: LoadingIcon, } ); return Icon; };
Now it's a separate chunk. Not ideal, but still better than having
50kb
in the first load.
Suggestion
If this object containing the dynamic imports were provided by
@tabler/icons-react
, webpack could statically analyze it and store potential network requests and data.This also eliminates the need to convert the iconsList to pascalCase on the end user's side.
const dynamicIconImports = { "a-b-2": () => import(`@tabler/icons-react/dist/esm/icons/IconAB2.mjs`), "a-b-off": () => import(`@tabler/icons-react/dist/esm/icons/IconABOff.mjs`), ... }
I will try to make a PR later.
I tested your solution from 3 weeks ago, but I felt the application significantly weighed down the page loading. So, I refactored it in a simpler and more direct way to achieve what I wanted, and I noticed an improvement in performance. I didn't have time to deeply analyze the reason for the improvement, but that's what I was able to do in the short time I had:
import { getIconInPascalCase } from '@/utils/icons/getIconInPascalCase'
import * as icons from '@tabler/icons-react'
import { memo } from 'react'
type Props = icons.TablerIconsProps & {
icon: string
}
type IconProps = (props: Props) => JSX.Element | null
export const DynamicIcon = memo<IconProps>(function DynamicIcon({
icon,
name,
...props
}) {
if (!icon) return null
const iconName = getIconInPascalCase(icon)
const Icon = icons[iconName] as unknown as IconProps
return <Icon icon={iconName} name={name} {...props} />
})
@tsnery This is because you are importing the entire namespace. Your app may feel faster, but you download every icon in the first-load 400kb gzipped
. You can test that with bundle-analyzer.
Is there a way to import the Icon dynamically in React? For example: