React component as prop: the right way™️
Tags: react, webdev, javascript, tutorial
Published: 2022-02-15 08:50:13

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fljxcldnv1oq2dyqtohr.png)


Originally published at [https://www.developerway.com](https://www.developerway.com). The website has more articles like this 😉

---

As always in React, there is one million way to do exactly the same thing. If, for example, I need to pass a component as a prop to another component, how should I do this? If I search the popular open-source libraries for an answer, I will find that:

- I can pass them as Elements like <a href="https://mui.com/components/buttons/#buttons-with-icons-and-label">Material UI library</a> does in Buttons with the `startIcon` prop
- I can pass them as components themselves like for example <a href="https://react-select.com/components">react-select library</a> does for its `components` prop
- I can pass them as functions like <a href="https://mui.com/components/data-grid/columns/#render-cell">Material UI</a> Data Grid component does with its `renderCell` prop

Not confusing at all 😅.

So which way is the best way and which one should be avoided? Which one should be included in some “React best practices” list and why? Let’s figure it out together!

Or, if you like spoilers, just scroll to the summary part of the article. There is a definitive answer to those questions 😉

## Why would we want to pass components as props?

Before jumping into coding, let’s first understand why we would want to pass components as props to begin with. Short answer: for flexibility and to simplify sharing data between those components.

Imagine, for example, we’re implementing a button with an icon. We could, of course, implement it like this:

```tsx
const Button = ({ children }: { children: ReactNode }) => {
  return (
    <button>
      <SomeIcon size="small" color="red" />
      {children}
    </button>
  );
};
```

But what if we need to give people the ability to change that icon? We could introduce `iconName` prop for that:

```tsx
type Icons = 'cross' | 'warning' | ... // all the supported icons

const getIconFromName = (iconName: Icons) => {
  switch (iconName) {
    case 'cross':
      return <CrossIcon size="small" color="red" />;
    ...
    // all other supported icons
  }
}
const Button = ({ children, iconName }: { children: ReactNode, iconName: Icons }) => {
  const icon = getIconFromName(name);

  return <button>
    {icon}
    {children}
  </button>
}
```

What about the ability for people to change the appearance of that icon? Change its size and color for example? We’d have to introduce some props for that as well:

```tsx
type Icons = 'cross' | 'warning' | ... // all the supported icons
type IconProps = {
  size: 'small' | 'medium' | 'large',
  color: string
};
const getIconFromName = (iconName: Icons, iconProps: IconProps) => {
  switch (iconName) {
    case 'cross':
      return <CrossIcon {...iconProps} />;
    ...
    // all other supported icons
  }
}
const Button = ({ children, iconName, iconProps }: { children: ReactNode, iconName: Icons, iconProps: IconProps }) => {
  const icon = getIconFromName(name, iconProps);

  return <button>
    {icon}
    {children}
  </button>
}
```

What about giving people the ability to change the icon when something in the button changes? If a button is hovered, for example, and I want to change icon’s color to something different. I’m not even going to implement it here, it’d be way too complicated: we’d have to expose `onHover` callback, introduce state management in every single parent component, set state when the button is hovered, etc, etc.

It’s not only a very limited and complicated API. We also forced our `Button` component to know about every icon it can render, which means the bundled js of this `Button` will not only include its own code, but also every single icon on the list. That is going to be one heavy button 🙂

This is where passing components in props come in handy. Instead of passing to the `Button` the detailed limited description of the `Icon` in form of its name and its props, our `Button` can just say: "gimme an `Icon`, I don't care which one, your choice, and I'll render it in the right place".

Let’s see how it can be done with the three patterns we identified at the beginning:

- passing as an Element
- passing as a Component
- passing as a Function

## Building a button with an icon

Or, to be precise, let’s build three buttons, with 3 different APIs for passing the icon, and then compare them. Hopefully, it will be obvious which one is better in the end. For the icon we’re going to use one of the icons from <a href="https://mui.com/components/icons/">material ui components library</a>. Lets start with the basics and just build the API first.

### First: icon as React Element

We just need to pass an element to the `icon` prop of the button and then render that icon near the children like any other element.

```tsx
type ButtonProps = {
  children: ReactNode;
  icon: ReactElement<IconProps>;
};

export const ButtonWithIconElement = ({ children, icon }: ButtonProps) => {
  return (
    <button>
      // our icon, same as children, is just React element 
      // which we can add directly to the render function
      {icon}
      {children}
    </button>
  );
};
```

And then can use it like this:

```tsx
<ButtonWithIconElement icon={<AccessAlarmIconGoogle />}>button here</ButtonWithIconElement>
```

### Second: icon as a Component

We need to create a prop that starts with a capital letter to signal it’s a component, and then render that component from props like any other component.

```tsx
type ButtonProps = {
  children: ReactNode;
  Icon: ComponentType<IconProps>;
};

export const ButtonWithIconComponent = ({ children, Icon }: ButtonProps) => {
  return (
    <button>
      // our button is a component 
      // its name starts with a capital letter to signal that 
      // so we can just render it here as any other
      component
      <Icon />
      {children}
    </button>
  );
};
```

And then can use it like this:

```tsx
import AccessAlarmIconGoogle from '@mui/icons-material/AccessAlarm';

<ButtonWithIconComponent Icon={AccessAlarmIconGoogle}>button here</ButtonWithIconComponent>;
```

### Third: icon as a function

We need to create a prop that starts with `render` to indicate it’s a render function, i.e. a function that returns an element, call the function inside the button and add the result to component’s render function as any other element.

```tsx
type ButtonProps = {
  children: ReactNode;
  renderIcon: () => ReactElement<IconProps>;
};

export const ButtonWithIconRenderFunc = ({ children, renderIcon }: ButtonProps) => {
  // getting the Element from the function
  const icon = renderIcon();
  return (
    <button>
      // adding element like any other element here
      {icon}
      {children}
    </button>
  );
};
```

And then use it like this:

```tsx
<ButtonWithIconRenderFunc renderIcon={() => <AccessAlarmIconGoogle />}>button here</ButtonWithIconRenderFunc>
```

That was easy! Now our buttons can render any icon in that special icon slot without even knowing what’s there. See the working example <a href="https://codesandbox.io/s/buttons-with-icon-24slt?file=/src/App.tsx">in the codesandbox</a>.

Time to put those APIs to a test.

## Modifying the size and color of the icon

Let’s first see whether we can adjust our icon according to our needs without disturbing the button. After all, that was the major promise of those patterns, isn’t it?

### First: icon as React Element

Couldn’t have been easier: all we need is just pass some props to the icon. We are using material UI icons, they give us `fontSize` and `color` for that.

```tsx
<ButtonWithIconElement icon={<AccessAlarmIconGoogle fontSize="small" color="warning" />}>button here</ButtonWithIconElement>
```

### Second: icon as a Component

Also simple: we need to extract our icon into a component, and pass the props there in the return element.

```tsx
const AccessAlarmIcon = () => <AccessAlarmIconGoogle fontSize="small" color="error" />;

const Page = () => {
  return <ButtonWithIconComponent Icon={AccessAlarmIcon}>button here</ButtonWithIconComponent>;
};
```

**Important**: the `AccessAlarmIcon` component should always be defined outside of the `Page` component, otherwise it will re-create this component on every `Page` re-render, and that is really bad for performance and prone to bugs. If you’re not familiar with how quickly it can turn ugly, this is the article for you: <a href="https://www.developerway.com/posts/how-to-write-performant-react-code">How to write performant React code: rules, patterns, do's and don'ts</a>

### Third: icon as a Function

Almost the same as the first one: just pass the props to the element.

```tsx
<ButtonWithIconRenderFunc
  renderIcon={() => (
    <AccessAlarmIconGoogle fontSize="small" color="success" />
  )}
>
```

Easily done for all three of them, we have infinite flexibility to modify the `Icon` and didn’t need to touch the button for a single thing. Compare it with `iconName` and `iconProps` from the very first example 🙂

## Default values for the icon size in the button

You might have noticed, that I used the same icon size for all three examples. And when implementing a generic button component, more likely than not, you’ll have some prop that control button’s size as well. Infinity flexibility is good, but for something as design systems, you’d want some pre-defined types of buttons. And for different buttons sizes, you’d want the button to control the size of the icon, not leave it to the consumer, so you won’t end up with tiny icons in huge buttons or vice versa by accident.

Now it’s getting interesting: is it possible for the button to control one aspect of an icon while leaving the flexibility intact?

### First: icon as React Element

For this one, it gets a little bit ugly. We receive our icon as a pre-defined element already, so the only thing we can do is to clone that element by using `React.cloneElement` api and override some of its props:

```tsx
// in the button component
const clonedIcon = React.cloneElement(icon, { fontSize: 'small' });

return (
  <button>
    {clonedIcon}
    {children}
  </button>
);
```

And at the consumer side we can just remove the `fontSize` property.

```tsx
<ButtonWithIconElement icon={<AccessAlarmIconGoogle color="warning" />} />
```

But what about default value, not overriding? What if I want consumers to be able to change the size of the icon if they need to?

Still possible, although even uglier, just nee to extract the passed props from the element and put them as default value:

```tsx
const clonedIcon = React.cloneElement(icon, {
  fontSize: icon.props.fontSize || 'small',
});
```

From the consumer side everything stays as it was before

```tsx
<ButtonWithIconElement icon={<AccessAlarmIconGoogle color="warning" fontSize="large" />} />
```

### Second: icon as a Component

Even more interesting here. First, we need to give the icon the default value on button side:

```tsx
export const ButtonWithIconComponent = ({ children, Icon }: ButtonProps) => {
  return (
    <button>
      <Icon fontSize="small" />
      {children}
    </button>
  );
};
```

And this is going to work perfectly when we pass the directly imported icon:

```tsx
import AccessAlarmIconGoogle from '@mui/icons-material/AccessAlarm';

<ButtonWithIconComponent Icon={AccessAlarmIconGoogle}>button here</ButtonWithIconComponent>;
```

`Icon` prop is nothing more than just a reference to material UI icon component here, and that one knows how to deal with those props. But we extracted this icon to a component when we had to pass to it some color, remember?

```tsx
const AccessAlarmIcon = () => <AccessAlarmIconGoogle fontSize="small" color="error" />;
```

Now the props' `Icon` is a reference to that wrapper component, and it just assumes that it doesn’t have any props. So our `fontSize` value from `<Icon fontSize="small" />` from the button will be just swallowed. This whole pattern, if you’ve never worked with it before, can be confusing, since it creates this a bit weird mental circle that you need to navigate in order to understand what goes where.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7wvfy3plrr8icdpiutuu.png)


In order to fix the icon, we just need to pass through the props that `AccessAlarmIcon` receives to the actual icon. Usually, it’s done via spread:

```tsx
const AccessAlarmIcon = (props) => <AccessAlarmIconGoogle {...props} color="error" />;
```

Or can be just hand-picked as well:

```tsx
const AccessAlarmIcon = (props) => <AccessAlarmIconGoogle fontSize={props.fontSize} color="error" />;
```


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v0keb5m2s1x46ut1pq8d.png)


While this pattern seems complicated, it actually gives us perfect flexibility: the button can easily set its own props, and the consumer can choose whether they want to follow the direction buttons gives and how much of it they want, or whether they want to do their own thing. If, for example, I want to override button’s value and set my own icon size, all I need to do is to ignore the prop that comes from the button:

```tsx
const AccessAlarmIcon = (props) => (
  // just ignore all the props coming from the button here
  // and override with our own values
  <AccessAlarmIconGoogle fontSize="large" color="error" />
);
```

### Third: icon as a Function

This is going to be pretty much the same as with icon as a Component, only with the function. First, adjust the button to pass settings to the `renderIcon` function:

```tsx
const icon = renderIcon({
  fontSize: 'small',
});
```

And then on the consumer side, similar to props in Component step, pass that setting to the rendered component:

```tsx
<ButtonWithIconRenderFunc renderIcon={(settings) => <AccessAlarmIconGoogle fontSize={settings.fontSize} color="success" />}>
  button here
</ButtonWithIconRenderFunc>
```

And again, if we want to override the size, all we need to do is to ignore the setting and pass our own value:

```tsx
<ButtonWithIconRenderFunc
  // ignore the setting here and write our own fontSize
  renderIcon={(settings) => <AccessAlarmIconGoogle fontSize="large" color="success" />}
>
  button here
</ButtonWithIconRenderFunc>
```

See <a href="https://codesandbox.io/s/buttons-with-icon-extended-vl6lo?file=/src/App.tsx">the codesandbox</a> with all three examples.

## Changing the icon when the button is hovered

And now the final test that should decide everything: I want to give the ability for the users to modify the icon when the button is hovered.

First, let’s teach the button to notice the hover. Just some state and callbacks to set that state should do it:

```tsx
export const ButtonWithIcon = (...) => {
  const [isHovered, setIsHovered] = useState(false);

  return (
    <button
      onMouseOver={() => setIsHovered(true)}
      onMouseOut={() => setIsHovered(false)}
    >
      ...
    </button>
  );
};
```

And then the icons.

### First: icon as React Element

That one is the most interesting of the bunch. First, we need to pass that `isHover` prop to the icon from the button:

```tsx
const clonedIcon = React.cloneElement(icon, {
  fontSize: icon.props.fontSize || 'small',
  isHovered: isHovered,
});
```

And now, interestingly enough, we created **exactly the same** mental circle that we had when we implemented “icon as Component”. We passed `isHover` property to the icon component, now we need to go to the consumer, wrap that original icon component into another component, that component will have `isHover` prop from the button, and it should return the icon we want to render in the button. 🤯 If you managed to understand that explanation from just words I’ll send you some chocolate 😅 Here’s some code to make it easier.

Instead of the original simple direct render of the icon:

```tsx
<ButtonWithIconElement icon={<AccessAlarmIconGoogle color="warning" />}>button here</ButtonWithIconElement>
```

we should create a wrapper component that has `isHovered` in its props and renders that icons as a result:

```tsx
const AlarmIconWithHoverForElement = (props) => {
  return (
    <AccessAlarmIconGoogle
      // don't forget to spread all the props!
      // otherwise you'll lose all the defaults the button is setting
      {...props}
      // and just override the color based on the value of `isHover`
      color={props.isHovered ? 'primary' : 'warning'}
    />
  );
};
```

And then render that new component in the button itself:

```tsx
<ButtonWithIconElement icon={<AlarmIconWithHoverForElement />}>button here</ButtonWithIconElement>
```

Looks a little bit weird, but it works perfectly 🤷🏽‍♀️

### Second: icon as a Component

First, pass the `isHover` to the icon in the button:

```tsx
<Icon fontSize="small" isHovered={isHovered} />
```

And then back to the consumer. And now the funniest thing ever. In the previous step we created exactly the same mental circle that we need to remember when we’re dealing with components passed as Components. And it’s not just the mental picture of data flow, I can literally **re-use exactly the same component** from the previous step here! They are just components with some props after all:

```tsx
<ButtonWithIconComponent Icon={AlarmIconWithHoverForElement}>button here</ButtonWithIconComponent>
```

💥 works perfectly.

### Third: icon as a Function

Same story: just pass the `isHovered` value to the function as the arguments:

```tsx
const icon = renderIcon({
  fontSize: 'small',
  isHovered: isHovered,
});
```

And then use it on the consumer side:

```tsx
<ButtonWithIconRenderFunc
  renderIcon={(settings) => (
    <AccessAlarmIconGoogle
      fontSize={settings.fontSize}
      color={settings.isHovered ? "primary" : "warning"}
    />
  )}
>
```

🎉 again, works perfectly.

Take a look at <a href="https://codesandbox.io/s/buttons-with-icon-and-hover-y4zgl?file=/src/App.tsx">the sandbox</a> with the working solution.

## Summary and the answer: which way is The Right Way™️?

If you read the full article, you’re probably saying right now: Nadia, aren’t they are basically the same thing? What’s the difference? You promised a clear answer, but I don’t see it 🙁 And you’re right.

And if you just scrolled here right away because you love spoilers: I’m sorry, I lied a bit for the sake of the story 😳. There is no right answer here.

All of them are more or less the same and you probably can implement 99% of the needed use cases (if not 100%) with just one pattern everywhere. The only difference here is semantics, which area has the most complexity, and personal preferences and religious beliefs.

If I _had to_ extract some general rules of which pattern should be used where, I’d probably go with something like this:

- I’d use “component as an Element” pattern (`<Button icon={<Icon />} />`) for cases, where I just need to render the component in a pre-defined place, without modifying its props in the “receiving” component.
- I’d use “component as a Component” pattern (`<Button Icon={Icon} />`) when I need to heavily modify and customise this component on the “receiving” side through its props, while at the same time allowing users full flexibility to override those props themselves (pretty much as <a href="https://react-select.com/components">react-select</a> does for `components` prop).
- I’d use “component as a Function” pattern (`<Button renderIcon={() => <Icon />} />`) when I need the consumer to modify the result of this function, depending on some values coming from the “host” component itself (pretty much what <a href="https://mui.com/components/data-grid/columns/#render-cell">Material UI Data Grid</a> component does with `renderCell` prop)

Hope this article made those patterns easier to understand and now you can use all of them when the use case needs it. Or you can now just totally ban any of them in your repo, just for fun or consistency sake, since now you can implement whatever you want with just one pattern 😊

See ya next time! ✌🏼


...

Originally published at [https://www.developerway.com](https://www.developerway.com). The website has more articles like this 😉

[Subscribe to the newsletter](https://www.developerway.com), [connect on LinkedIn](https://www.linkedin.com/in/adevnadia/) or [follow on Twitter](https://twitter.com/adevnadia) to get notified as soon as the next article comes out.