1

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'
DᴀʀᴛʜVᴀᴅᴇʀ
  • 7,681
  • 17
  • 73
  • 127

2 Answers2

1

Try something like this:

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

Since you are looping through the menu elements, icon is not interpreted as a React element because of the capitalization. In the destructuring above you are parsing icon as Icon so rendering it as a React component in <Icon /> should do the trick because it's the element itself.

There's another workaround that seems a little bit overkill but will work: you can always use a Map to hold the asset component, something like:

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

export default AssetComponentsTuple;

Your menu will become:

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

Then in the loop:

{menu.map(({link, name, icon}) => {
   let IconComponent = AssetComponentsTuple.get(icon);
    return (
      <Link key={name} to={link}>
        <div>
          <span className="icon"><IconComponent /></span>
          {name}
        </div>
      </Link>
    )
})}

You can even get rid of icon property and use directly the name in the AssetComponentsTuple, in that case: AssetComponentsTuple.get(name)

Ferran Buireu
  • 28,630
  • 6
  • 39
  • 67
1

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.

DᴀʀᴛʜVᴀᴅᴇʀ
  • 7,681
  • 17
  • 73
  • 127