Closed logemann closed 2 years ago
I think this depends on your build tool.
But something like this
const Icon = import(myIcon).then(Module => Module.default);
<Icon />
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'} />
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 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
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
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
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" />
);
};
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?
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.
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 />
}
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>
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 />
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 />
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?
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>
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>
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 ⭐
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.
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';
I'm trying to use this Icon in React Native, any help?
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:
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.
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:
^ 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:
version
prop)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.
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.