React component as prop: the right way™️

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 Material UI library does in Buttons with the prop
  • I can pass them as components themselves like for example react-select library does for its prop
  • I can pass them as functions like Material UI Data Grid component does with its 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:

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

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:

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 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 component to know about every icon it can render, which means the bundled js of this 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 the detailed limited description of the in form of its name and its props, our can just say: "gimme an , 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 material ui components library. 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 prop of the button and then render that icon near the children like any other element.

And then can use it like this:

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.

And then can use it like this:

Third: icon as a function

We need to create a prop that starts with 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.

And then use it like this:

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 in the codesandbox.

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 and for that.

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.

Important: the component should always be defined outside of the component, otherwise it will re-create this component on every 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: How to write performant React code: rules, patterns, do's and don'ts

Third: icon as a Function

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

Easily done for all three of them, we have infinite flexibility to modify the and didn't need to touch the button for a single thing. Compare it with and 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 api and override some of its props:

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

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:

From the consumer side everything stays as it was before

Second: icon as a Component

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

And this is going to work perfectly when we pass the directly imported 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?

Now the props’ is a reference to that wrapper component, and it just assumes that it doesn't have any props. So our value from 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.

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

Or can be just hand-picked as well:

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:

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 function:

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

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

See the codesandbox 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:

And then the icons.

First: icon as React Element

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

And now, interestingly enough, we created exactly the same mental circle that we had when we implemented “icon as Component”. We passed 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 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:

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

And then render that new component in the button itself:

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

Second: icon as a Component

First, pass the to the icon in the button:

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:

💥 works perfectly.

Third: icon as a Function

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

And then use it on the consumer side:

🎉 again, works perfectly.

Take a look at the sandbox 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 () 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 () 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 react-select does for prop).
  • I’d use “component as a Function” pattern () 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 Material UI Data Grid component does with 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 The website has more articles like this 😉

Subscribe to the newsletter, connect on LinkedIn or follow on Twitter to get notified as soon as the next article comes out.



Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Nadia Makarevich

Frontend architect, coder. Love solving problems, fixing things and writing in-depth tech articles: