tailwindlabs / heroicons

A set of free MIT-licensed high-quality SVG icons for UI development.
https://heroicons.com
MIT License
21.33k stars 1.28k forks source link

dynamic import heroicons react components #278

Closed logemann closed 2 years ago

logemann commented 3 years ago

Is there any way to make dynamic imports / usage of those react heroicon components?

Normal way: import {ChevronDownIcon} from "@heroicons/react/solid"; ... JSX: <ChevronDownIcon/>

Desired: import {${myName}} from "@heroicons/react/solid";

Of course this doesn't work but i just used it to express my goal. I know i could use a lib like react-svg and then do: <ReactSVG src="/images/icons/myName.svg" />

But then i would need to copy all those SVGs into a target folder but i am already using the react heroicons components elsewhere. Would like to stick only with the heroicons react lib.

sinnbeck commented 3 years ago

I think this depends on your build tool.

But something like this


const Icon = import(myIcon).then(Module => Module.default);

<Icon />
IT-MikeS commented 3 years ago

This is how I did it, quick but it works for what I needed.

// DynamicHeroIcon.tsx
// Simple Dynamic HeroIcons Component for React (typescript / tsx)
// by: Mike Summerfeldt (IT-MikeS - https://github.com/IT-MikeS)

import { FC } from 'react'
import * as HIcons from '@heroicons/react/outline'

const DynamicHeroIcon: FC<{icon: string}> = (props) => {
  const {...icons} = HIcons
  // @ts-ignore
  const TheIcon: JSX.Element = icons[props.icon]

  return (
    <>
      {/* @ts-ignore */}
      <TheIcon className="h-6 w-6 text-white" aria-hidden="true" />
    </>
  )
}

export default DynamicHeroIcon

Usage:

<DynamicHeroIcon icon={'CogIcon'} />
uvisgrinfelds commented 3 years ago

I needed dynamic import for Vue. Perhaps someone finds this useful:

<template>
  <component v-if="isLoaded" :is="heroIcons[name]" />
</template>

<script>
import * as heroIcons from "@heroicons/vue/solid";

export default {
  data() {
    return {
      isLoaded: false,
      heroIcons: heroIcons
    };
  },
  props: {
    name: String,
  },
  mounted() {
    this.isLoaded = true;
  },
};
</script>
creativeplus2 commented 3 years ago

This is how I did it, quick but it works for what I needed.

// DynamicHeroIcon.tsx
// Simple Dynamic HeroIcons Component for React (typescript / tsx)
// by: Mike Summerfeldt (IT-MikeS - https://github.com/IT-MikeS)

import { FC } from 'react'
import * as HIcons from '@heroicons/react/outline'

const DynamicHeroIcon: FC<{icon: string}> = (props) => {
  const {...icons} = HIcons
  // @ts-ignore
  const TheIcon: JSX.Element = icons[props.icon]

  return (
    <>
      {/* @ts-ignore */}
      <TheIcon className="h-6 w-6 text-white" aria-hidden="true" />
    </>
  )
}

export default DynamicHeroIcon

Usage:

<DynamicHeroIcon icon={'CogIcon'} />

How to convert this to JS react

IT-MikeS commented 3 years ago

This is how I did it, quick but it works for what I needed. ...

How to convert this to JS react

This is the JS version

// DynamicHeroIcon.jsx
// Simple Dynamic HeroIcons Component for React (javascript / jsx)
// by: Mike Summerfeldt (IT-MikeS - https://github.com/IT-MikeS)

import * as HIcons from '@heroicons/react/outline'

const DynamicHeroIcon = (props) => {
  const {...icons} = HIcons
  const TheIcon = icons[props.icon]

  return (
    <>
      <TheIcon className="h-6 w-6 text-white" aria-hidden="true" />
    </>
  )
}

export default DynamicHeroIcon
creativeplus2 commented 3 years ago

This is how I did it, quick but it works for what I needed. ...

How to convert this to JS react

This is the JS version

// DynamicHeroIcon.jsx
// Simple Dynamic HeroIcons Component for React (javascript / jsx)
// by: Mike Summerfeldt (IT-MikeS - https://github.com/IT-MikeS)

import * as HIcons from '@heroicons/react/outline'

const DynamicHeroIcon = (props) => {
  const {...icons} = HIcons
  const TheIcon = icons[props.icon]

  return (
    <>
      <TheIcon className="h-6 w-6 text-white" aria-hidden="true" />
    </>
  )
}

export default DynamicHeroIcon

Many Thanks Mike

peterwhite commented 3 years ago

Here is my take on the TypeScript version without ignores. This should only allow valid Heroicon names too:

import * as HeroIcons from "@heroicons/react/solid";

type IconName = keyof typeof HeroIcons;
interface IconProps {
    icon: IconName;
}

export const DynamicHeroIcon = ({ icon }: IconProps) => {
    const SingleIcon = HeroIcons[icon];

    return (
        <SingleIcon className="flex-shrink-0 w-5 h-5 text-gray-600" />
    );
};
ngekoding commented 3 years ago

I needed dynamic import for Vue. Perhaps someone finds this useful:

<template>
  <component v-if="isLoaded" :is="heroIcons[name]" />
</template>

<script>
import * as heroIcons from "@heroicons/vue/solid";

export default {
  data() {
    return {
      isLoaded: false,
      heroIcons: heroIcons
    };
  },
  props: {
    name: String,
  },
  mounted() {
    this.isLoaded = true;
  },
};
</script>

Do you know how to use it with defineAsyncComponent in Vue 3?

pickmanmurimi commented 2 years ago

I needed dynamic import for Vue. Perhaps someone finds this useful:

<template>
  <component v-if="isLoaded" :is="heroIcons[name]" />
</template>

<script>
import * as heroIcons from "@heroicons/vue/solid";

export default {
  data() {
    return {
      isLoaded: false,
      heroIcons: heroIcons
    };
  },
  props: {
    name: String,
  },
  mounted() {
    this.isLoaded = true;
  },
};
</script>

This works realy great @uvisgrinfelds , many thanks.

leocabrallce commented 2 years ago

An approach using Next dynamic imports:

import dynamic from 'next/dynamic'

export default function MenuItem(props) {
  const Icon = dynamic(() =>
    import('@heroicons/react/solid').then((mod) => mod[props.icon])
  )

  return <Icon />
}
abhisheksatre commented 2 years ago

This is how I achieved in Vue 3

<template>
    <component :is="getIcon('HomeIcon')"/>
</template>

<script>
import {defineAsyncComponent} from 'vue'

export default {
        setup() {

            function getIcon(iconName) {
                return defineAsyncComponent(() => import('@heroicons/vue/outline/'+iconName));        
            }

            return {
                getIcon
            }
        }
    }

</script>
pawelmalak commented 2 years ago

Here is my take on the TypeScript version without ignores. This should only allow valid Heroicon names too:

import * as HeroIcons from "@heroicons/react/solid";

type IconName = keyof typeof HeroIcons;
interface IconProps {
    icon: IconName;
}

export const DynamicHeroIcon = ({ icon }: IconProps) => {
    const SingleIcon = HeroIcons[icon];

    return (
        <SingleIcon className="flex-shrink-0 w-5 h-5 text-gray-600" />
    );
};

Extended version with styling and both outline and solid icons:

import * as SolidIcons from '@heroicons/react/solid';
import * as OutlineIcons from '@heroicons/react/outline';

export type IconName = keyof typeof SolidIcons | keyof typeof OutlineIcons;

interface Props {
  icon: IconName;
  className?: string;
  outline?: boolean;
}

export const HeroIcon = (props: Props): JSX.Element => {
  const { icon, className = 'w-6 h-6 text-gray-600', outline = false } = props;

  const Icon = outline ? OutlineIcons[icon] : SolidIcons[icon];

  return <Icon className={className} />;
};

Usage:

<HeroIcon icon='CheckCircleIcon' />
<HeroIcon icon='CheckCircleIcon' className='h-8 w-8 text-green-600' />
<HeroIcon icon='CheckCircleIcon' className='h-7 w-7 text-green-600' outline />
kyleknighted commented 2 years ago

To pile on, thanks to @leocabrallce & @pawelmalak - here is Next.js with Typescript

import dynamic from "next/dynamic";
import { ComponentType } from "react";
import { IconName } from "types/Icon";

type Props = {
  name: IconName;
  className?: string;
  outline?: boolean;
};

const HeroIcon = ({ name, className = "", outline = false }: Props) => {
  const Icon: ComponentType<{ className: string }> = outline
    ? dynamic(() => import("@heroicons/react/outline").then((mod) => mod[name]))
    : dynamic(() => import("@heroicons/react/solid").then((mod) => mod[name]));

  return <Icon className={className} aria-hidden={true} />;
};

export default HeroIcon;

and can be used like above

<HeroIcon icon='CheckCircleIcon' />
<HeroIcon icon='CheckCircleIcon' className='h-8 w-8 text-green-600' />
<HeroIcon icon='CheckCircleIcon' className='h-7 w-7 text-green-600' outline />
mquadrat commented 2 years ago

Found this issue after searching the web to no avail. Was almost giving up. May I suggest to add that the best solutions for vue and react to the docs?

stevebauman commented 2 years ago

Here's another Vue component handling both styles if this is useful to anyone (came here from a Google search as well):

<template>
    <component :is="icon" />
</template>

<script>
import * as solid from '@heroicons/vue/solid';
import * as outline from '@heroicons/vue/outline';

export default {
    props: {
        name: String,
        outline: Boolean,
    },

    computed: {
        icons() {
            return { solid, outline };
        },

        icon() {
            return this.icons[this.outline ? 'outline' : 'solid'][this.name];
        },
    },
};
</script>
oskery commented 2 years ago

To build upon previous Vue answers. Using icons conditionally and dynamically inside e.g. navigation:

<template>
  <router-link v-for="{ href, icon, label } in links" :key="href" :to="href">
    <component
      :is="getIcon(icon, $route.path === href ? 'solid' : 'outline')"
      class="w-4"
    />
    {{ label }}
  </router-link>
</template>

<script>
import * as solid from '@heroicons/vue/solid'
import * as outline from '@heroicons/vue/outline'

export default {
  computed: {
    icons() {
      return { solid, outline }
    },
    links() {
       return [
         { href: '/users', icon: 'UsersIcon', label: 'Users' },
         { href: '/businesses', icon: 'OfficeBuildingIcon', label: 'Businesses' },
       ]
    }
  },
  methods: {
    getIcon(name, type) {
      return this.icons[type][name]
    },
  }
}
</script>
naquiroz commented 2 years ago

I made a library that does dynamic importing (in react). You can get it here. Feel free to contribute or add a star if it helped you ⭐

RobinMalfait commented 2 years ago

Hey! Thank you for your question! Much appreciated! 🙏

Seems like there are a bunch of awesome solutions in this issue! Going to close this since this isn't something we have to "fix" in the Heroicons library.

cellulosa commented 2 years ago

To pile on, thanks to @leocabrallce & @pawelmalak - here is Next.js with Typescript

Thanks for this, @stevebauman, though where do you get this from? import { IconName } from 'types/Icon';

SamZing777 commented 2 years ago

I'm trying to use this Icon in React Native, any help?

lorenzogm commented 1 year ago

To pile on, thanks to @leocabrallce & @pawelmalak - here is Next.js with Typescript

import dynamic from "next/dynamic";
import { ComponentType } from "react";
import { IconName } from "types/Icon";

type Props = {
  name: IconName;
  className?: string;
  outline?: boolean;
};

const HeroIcon = ({ name, className = "", outline = false }: Props) => {
  const Icon: ComponentType<{ className: string }> = outline
    ? dynamic(() => import("@heroicons/react/outline").then((mod) => mod[name]))
    : dynamic(() => import("@heroicons/react/solid").then((mod) => mod[name]));

  return <Icon className={className} aria-hidden={true} />;
};

export default HeroIcon;

and can be used like above

<HeroIcon icon='CheckCircleIcon' />
<HeroIcon icon='CheckCircleIcon' className='h-8 w-8 text-green-600' />
<HeroIcon icon='CheckCircleIcon' className='h-7 w-7 text-green-600' outline />

Did you folks check the bundle size with that approach? I'm getting all the icons and I just use a couple:

image

whitespacecode commented 1 year ago

Hey! Thank you for your question! Much appreciated! 🙏

Seems like there are a bunch of awesome solutions in this issue! Going to close this since this isn't something we have to "fix" in the Heroicons library.

Maybe not.. But please provide some updated docs on how to use them dynamically for Vue - Typescript - React. It was also the first thing i needed, gladly i found this topic.

kaelansmith commented 1 year ago

To pile on, thanks to @leocabrallce & @pawelmalak - here is Next.js with Typescript

import dynamic from "next/dynamic";
import { ComponentType } from "react";
import { IconName } from "types/Icon";

type Props = {
  name: IconName;
  className?: string;
  outline?: boolean;
};

const HeroIcon = ({ name, className = "", outline = false }: Props) => {
  const Icon: ComponentType<{ className: string }> = outline
    ? dynamic(() => import("@heroicons/react/outline").then((mod) => mod[name]))
    : dynamic(() => import("@heroicons/react/solid").then((mod) => mod[name]));

  return <Icon className={className} aria-hidden={true} />;
};

export default HeroIcon;

and can be used like above

<HeroIcon icon='CheckCircleIcon' />
<HeroIcon icon='CheckCircleIcon' className='h-8 w-8 text-green-600' />
<HeroIcon icon='CheckCircleIcon' className='h-7 w-7 text-green-600' outline />

Did you folks check the bundle size with that approach? I'm getting all the icons and I just use a couple:

image

^ I discovered this bundle size issue too. One solution that allows the same "dynamic" ability but that doesn't add ANY icons to the bundle (even the ones you use), is using a CDN. I created this component that abstracts the use of the jsdelivr CDN:

import { useEffect, useState } from 'react';

const HeroIcon = ({
  icon,
  outline = false,
  mini = false,
  version = '2.0.12',
  className = 'w-6 h-6 text-slate-600',
  ...props
}) => {
  const [svg, setSvg] = useState(null);
  const [isLoaded, setIsLoaded] = useState(false);
  const [isErrored, setIsErrored] = useState(false);

  useEffect(() => {
    const url = `https://cdn.jsdelivr.net/npm/heroicons@${version}/${version.startsWith('2') && (mini ? '20/' : '24/')}${outline ? 'outline' : 'solid'}/${icon}.svg`;
    fetch(url)
      .then((res) => res.text())
      .then(setSvg)
      .catch(setIsErrored)
      .then(() => setIsLoaded(true));
  }, [icon, mini, outline, version]);

  return (
    <div
      className={className}
      dangerouslySetInnerHTML={{ __html: svg }}
      {...props}
    />
  );
};

// use like so:
<HeroIcon icon="plus-circle" className="w-5 h-5 text-blue-500" />
<HeroIcon icon="plus-circle" outline={true} onClick={() => console.log('do something')} />

// use an icon from version 1:
<HeroIcon version="1.0.6" icon="document-text" />

Pros:

Cons:

For my specific project, the pros far outweigh the cons. I'm curious if anyone sees other cons with this approach, or if anyone is smart enough to figure out how to exclude unused icons from the bundle with the NPM package approach.