What is the React way of inserting an icon into another component?

Viewed 287

I'm trying to create an WithIcon wrapper component which would insert a child (icon) into a wrapped component.

Let's say I have a button:

<Button>Add item</Button>

I want to create a component WithIcon which will be used like this:

<WithIcon i="plus"><Button>Add item</Button></WithIcon>

Ultimately what I want to achieve is this:

<Button className="with-icon"><i className="me-2 bi bi-{icon}"></i>Add item</Button>

Notice the added className and the tag within the Button's body.

I'm trying to figure out how the WithIcon component's code should look like. What is the React way of achieving this result?

3 Answers

The hardest part was the rules of using the WithIcon Will we only have one ? Will we have only it at the leftmost ? Something like that.

But if we follow your example. We can relatively write something like this for the WithIcon

const WithIcon = ({ i, children }) => {
  return React.Children.map(children, (child) => {
    return (
      <>
        <i className={`me-2 bi bi-${i}`}></i>
        {React.cloneElement(child, { className: "with-icon" })}
      </>
    );
  });
};

Then we can just use it the way you want it

<WithIcon i="plus"><Button>Add item</Button></WithIcon>

What we do is just looping through the children which in react is any nested jsx you throw in it (Button in our case)

You can find my fiddle here : https://codesandbox.io/s/react-font-awesome-forked-321tz?file=/src/index.js

UPDATE So my previous answer does not fully meet the end result we want. The will need to be the main parent

The idea is still quite the same as before but here we are infering the type of the component we passed inside the WithIcon This also adds a safeguard when we passed a nested component inside the WithIcon

const WithIcon = ({ i, children }) => {
  return React.Children.map(children, (child) => {
    const MyType = child.type; // So we can get the Button
    return (
      <MyType className="with-icon">
        <i className={`me-2 bi bi-${i}`}></i>
        {(React.cloneElement(child, {}), [child.props.children])}
      </MyType>
    );
  });
};

I think I'll go to sleep I'll update the rest of the explanation at later date. See the fiddle here : https://codesandbox.io/s/react-font-awesome-forked-y43fx?file=/src/components/WithIcon.js

Note that this code does not preserved the other props of the passed component, but you can relatively add that by adding {...child.props} at the MyComponent which is just (reflection like?) of infering the component.

Of course also have another option like HOC Enhancers to do this but that adds a bit of complexity to your how to declare your component api. So Pick whats best for ya buddy

Maybe try using a higher order component?

const withIcon = (icon, Component) => ({children, ...props}) => {
    return (
        <Component className="with-icon" {...props}>
            <i className=`me-2 bi bi-${icon}` /> 
            {children}
        </Component>
    );
}

Then the usage is

const ButtonWithIcon = withIcon("your-icon", Button);

<ButtonWithIcon>Add Item</ButtonWithIcon>

From my experience with react it usually comes down to either using a property inside the component like here (https://material-ui.com/api/button/) or higher order component like what I described.

There are two common patterns used in React for achieving this kind of composition:

Higher-Order Components

Start by defining a component for your button:

const Button = ({ className, children }) => (
  <button className={className}>{children}</button>
);

Then the higher-order component can be implemented like this:

const withIcon = (Component) => ({ i, className = '', children, ...props }) => (
  <Component {...props} className={`${className} with-icon`}>
    <i className={`me-2 bi bi-${i}`} />
    {children}
  </Component>
);

Usage:

const ButtonWithIcon = withIcon(Button);

<ButtonWithIcon i="plus">Add Item</ButtonWithIcon>

Context

Start by defining the context provider for the icon:

import { createContext } from 'react';

const Icon = createContext('');

const IconProvider = ({ i, children }) => (
  <Icon.Provider value={i}>{children}</Icon.Provider>
);

and then your component:

import { useContext } from 'react';

const Button = ({ className = '', children }) => {
  const i = useContext(Icon);

  if (i) {
    className += ' with-icon';
    children = (
      <>
        <i className={`me-2 bi bi-${i}`} />
        {children}
      </>
    );
  }

  return (
    <button className={className}>{children}</button>
  );
};

Usage:

<IconProvider i="plus"><Button>Add Item</Button></IconProvider>
Related