tabler / tabler-icons

A set of over 5400 free MIT-licensed high-quality SVG icons for you to use in your web projects.
https://tabler.io/icons
MIT License
17.84k stars 886 forks source link

Get Icon dynamically #998

Open tsnery opened 6 months ago

tsnery commented 6 months ago

Is there a way to import the Icon dynamically in React? For example:

\

jcv8000 commented 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
            }}
        />
    );
}
tsnery commented 6 months ago

Have you tried rendering a list of all these icons?

nzyoni commented 5 months ago

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.

timheerwagen commented 5 months ago

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 />;
};
timheerwagen commented 4 months ago

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],"

image

The icons are bundled in individual chunks, which I think is correct behavior. image

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, the 50kb 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.

image

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.

tsnery commented 4 months ago

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],"

image

The icons are bundled in individual chunks, which I think is correct behavior. image

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, the 50kb 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.

image

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} />
})
timheerwagen commented 4 months ago

@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.