Search code examples
reactjsdynamicmenugatsbyreact-icons

In Gatsby how to render React Icons dynamically from gatsby-config's menuLinks?


Following Gatsby's doc on Creating Dynamic Navigation in Gatsby I created a barebones menu and wanted to see if I can add React Icons' components to it:

gatsby-config.js (stripped down)

menuLinks:[
  {
      name:'home',
      icon: 'AiFillHome',
      link:'/'
  },
  {
      name:'contact',
      icon: 'AiFillHome',
      link:'/contact'
  }
]

after finding out that Gatsby errors out when I tried creating an external menuLinks file as a module, example:

failed approach as module:

import React from 'react'

// React Icons
import { AiFillHome } from 'react-icons/ai'

const Menu = [
  {
      name:'home',
      icon: <AiFillHome />,
      link:'/'
  },
  {
      name:'contact',
      icon: <AiFillHome />,
      link:'/contact'
  }
]

export default Menu

I dont have an issue in my query:

const data = useStaticQuery(
  graphql`
    query {
      site {
        siteMetadata {
          menuLinks {
            name
            icon
            link
          }
        }
      }
    }
  `,
)

in a file I've passed down menu props to from my query and then map.

(stripped down file):

{menu.map((menuItem, key) => {
    return (
      <Link key={key} to={menuItem.link}>
        <div>
          <span className="icon">{`<${menuItem.icon} />`}</span>
          {menuItem.name}
        </div>
      </Link>
    )
})}

my icon doesn't render. I've also tried:

<span className="icon" component={menuItem.icon} />

doesn't work. I've also tried:

<span className="icon"><menuItem.icon /></span>

and:

<span className="icon">{React.createElement(menuItem.icon)}</span>

Research:

In Gatsby how can I pass an icon's component name to menuLinks and later render it?

Edit

After the answer the implementation of:

{menu.map(({link, name, icon: Icon}) => {
    return (
      <Link key={name} to={link}>
        <div>
          <span className="icon"><Icon /></span>
          {name}
        </div>
      </Link>
    )
})}

the browser doesn't render the React Icon Component and when examining the Elements panel I get:

<span class="icon">
  <aifillhome></aifillhome>
</span>

Also note I do get a terminal error of:

warning 'AiFillHome' is defined but never used no-unused-vars

which was expected but that leads to me wondering how do I bring in:

import { AiFillHome } from 'react-icons/ai'

Solution

  • Implementation of:

    const AssetComponentsTuple = new Map([
      [`home`, <AiFillHome />],
      [`otherAsset`, <AiFillHome2 />],
    ])
    
    export default AssetComponentsTuple
    

    would throw an error of:

    Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.

    but the approach gave me an idea for this workaround of:

    {menu.map(({link, name, icon}) => {
        return (
          <Link key={name} to={link}>
            <div>
              <span className="icon"><IconRetrieve icon={icon} /></span>
              {name}
            </div>
          </Link>
        )
    })}
    

    and that allowed me to pass the icon string and ternary for the component.

    IconRetrieve component:

    import React from 'react'
    import PropTypes from 'prop-types'
    
    // React Icons
    import { AiFillHome } from 'react-icons/ai'
    import { MdCall } from 'react-icons/md'
    import { BiErrorCircle } from 'react-icons/bi'
    
    const IconRetrieve = ({ icon }) => {
      const mapIcons = new Map([
        [`home`, <AiFillHome />],
        [`contact`, <MdCall />],
        [`default`, <BiErrorCircle />],
      ])
      return mapIcons.has(icon) ? mapIcons.get(icon) : mapIcons.get('default')
    }
    
    IconRetrieve.propTypes = {
      icon: PropTypes.string.isRequired,
    }
    
    export default IconRetrieve
    

    The positive of this approach is I can create SVG components or any component in that matter and return it based on the icon string.