Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get Icon dynamically #998

Closed
tsnery opened this issue Feb 8, 2024 · 9 comments · Fixed by #1081
Closed

Get Icon dynamically #998

tsnery opened this issue Feb 8, 2024 · 9 comments · Fixed by #1081
Labels
support-request Help/support requested

Comments

@tsnery
Copy link

tsnery commented Feb 8, 2024

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

<Icon name={iconName} />

@jcv8000
Copy link

jcv8000 commented Feb 9, 2024

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
Copy link
Author

tsnery commented Feb 12, 2024

Have you tried rendering a list of all these icons?

@BG-Software-BG BG-Software-BG added the support-request Help/support requested label Feb 19, 2024
@nzyoni
Copy link

nzyoni commented Mar 4, 2024

I support the addition of this capability 👍

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
Copy link
Contributor

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
Copy link
Contributor

timheerwagen commented Apr 3, 2024

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
Copy link
Author

tsnery commented Apr 4, 2024

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
Copy link
Contributor

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

@Groompel
Copy link

Groompel commented Sep 29, 2024

Hey guys! Thank you for this update, it was just in time when I started integrating this into my project.

I may be confusing something, but I couldn't get it to work when running a development environment. When I import dynamicImports from @tabler/icons-react it adds additional 12,000 modules to the dev bundle and compiles a page in a whopping 1.5 minutes.

Is this a correct way to do this?

'use client';

import {
  IconFileUnknown,
  IconLoader2,
  dynamicImports,
} from '@tabler/icons-react';
import dynamic from 'next/dynamic';

const LoadingIcon = () => (
  <IconLoader2 className="animate-pulse" aria-label="Icon is loading..." />
);

type TIconType = keyof typeof dynamicImports.default;

export const getDynamicIcon = (icon: TIconType) => {
  const Icon = dynamic(
    async () => {
      try {
        const componentImport = await dynamicImports.default[icon]();

        if (!componentImport) {
          return IconFileUnknown;
        }

        return componentImport.default;
      } catch (error) {
        return IconFileUnknown;
      }
    },
    {
      loading: LoadingIcon,
    }
  );

  return Icon;
};

Update

I figured this out when I looked closer into @timheerwagen's solution, what a dumb mistake 🤦‍♂️
For anyone who might stumble upon the same issue the correct way to import it is this:

 const importsMap = (await import('@tabler/icons-react')).dynamicImports.default;

return importsMap[icon as keyof typeof importsMap]().catch(
  () => IconFileUnknown
);

Also if you want to add global Typescript icon type support to your IDE, add this to your root d.ts file:

import { dynamicImports } from '@tabler/icons-react';

declare global {
  declare type TIconType = keyof typeof dynamicImports.default;
}

@naveen7555-sl
Copy link

is there way to render all the icons instead of mentioning names every time, i wanted to list all icons in my app?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
support-request Help/support requested
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants