How can I dynamically load an icon using its snake_case name (React, material-ui)
Asked Answered
P

8

19

Normally I'd use material-ui icons by importing them directly per the material-ui instructions.

But I have a text tag, which is the actual icon name (like calendar_view_day) and need to get and render an icon component from it dynamically.

How can I something like:

render() {
  return (
    <Icon name="calendar_view_day" color="primary" />
  )
}

Instead of:

render() {
  return (
    <CalendarViewDay color="primary" />  // Imported by name
  )
}
Pomatum answered 29/5, 2019 at 22:43 Comment(0)
H
6

Folks it's almost 2024, use Font Material Icon. Super lightweight:

npm install material-icons@latest

Import in JS (example: src/index.js in Create React App src/_app.js in NextJS, src/main.js in Vue CLI):

import 'material-icons/iconfont/material-icons.css';

In your component:

import { Icon} from '@mui/material';

...

<Icon>{name}</Icon>

Just be aware icon name are small case and underscore separated

Hudnall answered 16/7, 2023 at 15:46 Comment(2)
This creates a <span /> and not a <svg />. This may not be a big deal in a lot of cases but sadly a deal breaker for my use case.Amoral
How are you able to retrieve a list of icon names that match a specific query? This is what I cannot find documentation on.Upsilon
P
21

Later Edits

EDIT 2: Look at @Roozbeh's more up-to-date mid-2023 answer on using Font Material Icon before using the following.


EDIT 1: This answer is for MUI v4, MUI v5 answers below (but untested by me)

Correct answer

Turns out material-ui includes an icon component that allows you to do this... and it converts names itself, so accepts snake, pascal and other variants. BEWARE this will massively increase bundle size, as pointed out in the comments. If you're bundle size constrained, you'll have to take a different approach of serving the icon SVGs from somewhere!

import Icon from '@material-ui/core/Icon'

...

render() {
  return (
    <Icon>{props.iconName}</Icon>
  )
}

Previous answer (working but massive overkill)

Create function to:

...Then use the material-ui Icon component.

Here's the code:

import Icon from '@material-ui/core/Icon'

function upperFirst(string) {
  return string.slice(0, 1).toUpperCase() + string.slice(1, string.length)
}

function fixIconNames(string) {
  const name = string.split('_').map(upperFirst).join('')
  if (name === '3dRotation') {
    return 'ThreeDRotation'
  } else if (name === '4k') {
    return 'FourK'
  } else if (name === '360') {
    return 'ThreeSixty'
  }
  return name
}

...

render() {
  const iconName = fixIconNames(props.iconName)
  return (
    <Icon>{props.iconName}</Icon>
  )
}
Pomatum answered 29/5, 2019 at 22:43 Comment(6)
importing the whole Icon module from material-ui will resolve the functionality, but will explode the bundle! You can analyze the bundle size via React official guidelineSheerness
Thanks @Navid, added a health warnign to my answer.Pomatum
This solution uses font icons instead of svg icons. Requires the used font to be loaded. See: material-ui.com/components/icons/#icon-font-iconsVaricose
The 'correct answer' only prints the string props.iconName for me, doesn't show the actual icon.Panegyrize
@jonasRosenvist, you also need to include some link into your code: <link rel="stylesheet" href="fonts.googleapis.com/icon?family=Material+Icons" />Clipping
this solution worked for me - <Icon>save</Icon>; The icon name should be lower case without the "Icon" suffix. Something like ChevronRight - should be changed to chevron_right. You have import the material_icon CSS file in index.jsOldie
H
6

Folks it's almost 2024, use Font Material Icon. Super lightweight:

npm install material-icons@latest

Import in JS (example: src/index.js in Create React App src/_app.js in NextJS, src/main.js in Vue CLI):

import 'material-icons/iconfont/material-icons.css';

In your component:

import { Icon} from '@mui/material';

...

<Icon>{name}</Icon>

Just be aware icon name are small case and underscore separated

Hudnall answered 16/7, 2023 at 15:46 Comment(2)
This creates a <span /> and not a <svg />. This may not be a big deal in a lot of cases but sadly a deal breaker for my use case.Amoral
How are you able to retrieve a list of icon names that match a specific query? This is what I cannot find documentation on.Upsilon
L
5

MUI 5 developers here you go.

import * as Muicon from "@mui/icons-material";
const Icon = Muicon['SatelliteAlt']
<Icon fontSize="large" sx={{ px: 1 }}/>

Typescript

import * as MUIcon from "@mui/icons-material";
interface IconProps {
   icon?: keyof typeof MUIcon;
}
const IconComp: React.FC<IconProps> = ({
  icon,
}) => {
    const Icon = icon && MUIcon[icon];
    return ({Icon && <Icon />})
}
Licht answered 28/9, 2022 at 11:59 Comment(3)
Thanks Sufiyan, edited my answer to point to this for v5.Pomatum
The return line in IconComp results in a parsing (unexpected token) error when compiling (and it's not clear to me what this is doing or how it is intended to be used).Kabyle
The bad thing about this solution is that you are importing every mui icon, adding 4Mb to the budle.Bughouse
C
4
import React, {createElement } from "react";
import * as MuiIcons from "@material-ui/icons";

export default ({ iconName }) => (
  <div>
      {createElement(MuiIcons[iconName])}
  </div>
);


<ComponentWithIcon iconName="Add" />
<ComponentWithIcon iconName="Anchor" />
Copalite answered 22/6, 2022 at 6:2 Comment(1)
Your approach is simple but inefficient because it includes all icons in the bundle.Percutaneous
E
2

Here is a component that allow you to create a component from string like this :

<MIcon name={'AccessAlarms'} sx={{ mr: 0.5 }}/>

Component in typescript :

import { IconTypeMap } from '@mui/material'
import * as MUIcon from '@mui/icons-material'

type IconProps = IconTypeMap['props']

interface MIconProps extends IconProps {
  name: string | undefined
}

export default function MIcon(props: MIconProps) {
  const { name } = props
  const Icon = MUIcon[name as keyof typeof MUIcon]
  if (Icon == null) {
    throw `There is no "${name}" Icon`
  }
  return <Icon {...props} />
}

You can pick the name of the icon from https://mui.com/material-ui/material-icons/

Other generic Icon props are also available in this component such as sx.

Elea answered 4/7, 2023 at 7:58 Comment(0)
A
0

As stated in the comments to thclark's answer above, the solution using the Icon component will use the font version of the Material-UI icons, which doesn't contain the full icon-set.

By using the same method to get the correct icon name (borrowed from thclark) combined with React.createElement does the trick. I did this in TypeScript, it's even easier in vanilla javascript. Here comes the component in TS version (dynamicIcon.tsx), also available in this repo:

import * as React from 'react';
import * as Icons from '@material-ui/icons/';

type IconType = typeof import('@material-ui/icons/index');

interface DynamicIconProps {
    iconName: string,
    className: string
}

function upperFirst(string:string) {
    return string.slice(0, 1).toUpperCase() + string.slice(1, string.length)
}
  
function fixIconNames(string:string) {
    const name = string.split('-').map(upperFirst).join('')
    if (name === '3dRotation') {
        return 'ThreeDRotation'
    } else if (name === '4k') {
        return 'FourK'
    } else if (name === '360') {
        return 'ThreeSixty'
    }
    return name;
}

export default class DynamicIcon extends React.Component<DynamicIconProps> {
    constructor(props: DynamicIconProps) {
        super(props);
    }

    render() {
        return React.createElement(Icons[fixIconNames(this.props.iconName)! as keyof IconType], {className: this.props.className});
    }
}
Allium answered 25/1, 2021 at 23:39 Comment(1)
Well done for providing a TS solution. But the creation of the element was never the problem - it's the bundle size. I don't use TS but as far as I can see, in order to work, this must still import every single icon into the bundle (check create-react-app.dev/docs/analyzing-the-bundle-size).Pomatum
D
0

Importing whole Icons from @mui increasing my build size by 4MBs which is something I wasn't looking for, so finally I solved it like this

import Icon from "@material-ui/core/Icon";
import * as React from "react";



import { Props } from "./types";


function cameltoSnakeCase(text = "") {
    return text
        .replace(/([A-Z])/g, "_$1")
        .toLowerCase()
        .slice(1);
}


const IconPresentation = ({ glyph, sx }: Props) => {
    if (!glyph) return null;
    return <Icon style={{ ...sx }}>{cameltoSnakeCase(glyph?.toString())}</Icon>;
};


export default IconPresentation;
Diseased answered 28/6, 2023 at 21:52 Comment(0)
K
0

You can achieve it this way

import { SvgIconComponent } from '@mui/icons-material';

const MUIcon: { [key: string]: SvgIconComponent } = require('@mui/icons-material');

const CompA = () => {
  const renderIcon = (iconName: string) => {
    const Icon = MUIcon[iconName];

    return (
       {Icon && <Icon />}
    )
  }
}
Krems answered 7/8, 2023 at 12:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.