React Pass Props to Component Again
React component every bit prop: the right way™️
Nadia Makarevich
February 15, 2022
As always in React, in that location is 1 million way to do exactly the same thing. If, for example, I need to pass a component as a prop to some other component, how should I do this? If I search the pop open-source libraries for an reply, I will detect that:
- I can pass them as Elements similar Material UI library does in Buttons with the
startIconprop - I tin can pass them as components themselves like for example react-select library does for its
componentsprop - I tin pass them as functions like Textile UI Data Grid component does with its
renderCellprop
Non confusing at all 😅.
So which way is the all-time way and which one should be avoided? Which one should exist included in some "React best practices" list and why? Let's figure it out together!
Or, if you lot similar spoilers, only scroll to the summary part of the article. In that location is a definitive answer to those questions 😉
Why would we want to pass components as props?
Before jumping into coding, let'due south first empathize why we would want to laissez passer components every bit props to begin with. Short answer: for flexibility and to simplify sharing information between those components.
Imagine, for instance, we're implementing a button with an icon. We could, of course, implement it like this:
ane const Push button = ( { children } : { children : ReactNode } ) => {
2 render (
3 < push >
4 < SomeIcon size = " small " color = " red " />
5 { children }
6 </ button >
7 ) ;
viii } ;
Merely what if nosotros need to requite people the ability to alter that icon? We could introduce iconName prop for that:
1 blazon Icons = 'cross' | 'alert' | ...
2
3 const getIconFromName = ( iconName : Icons ) => {
4 switch ( iconName ) {
5 case 'cross' :
6 return < CrossIcon size = " small " color = " red " /> ;
7 ...
8
9 }
10 }
11 const Button = ( { children , iconName } : { children : ReactNode , iconName : Icons } ) => {
12 const icon = getIconFromName ( name ) ;
13
14 return < push >
15 { icon }
xvi { children }
17 </ button >
18 }
What about the ability for people to modify the advent of that icon? Change its size and colour for example? We'd have to introduce some props for that as well:
1 type Icons = 'cross' | 'alert' | ...
2 type IconProps = {
3 size : 'pocket-size' | 'medium' | 'big' ,
four color : string
5 } ;
half dozen const getIconFromName = ( iconName : Icons , iconProps : IconProps ) => {
7 switch ( iconName ) {
8 case 'cross' :
9 return < CrossIcon { ... iconProps } /> ;
ten ...
eleven
12 }
thirteen }
14 const Push = ( { children , iconName , iconProps } : { children : ReactNode , iconName : Icons , iconProps : IconProps } ) => {
15 const icon = getIconFromName ( name , iconProps ) ;
16
17 return < push >
18 { icon }
xix { children }
20 </ button >
21 }
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'southward color to something unlike. I'yard not even going to implement information technology here, it'd exist mode too complicated: we'd have to betrayal onHover callback, introduce country management in every unmarried parent component, prepare state when the button is hovered, etc, etc.
It'due south non only a very limited and complicated API. We besides forced our Push component to know nigh every icon it tin can render, which means the bundled js of this Button will not only include its ain code, but likewise every single icon on the list. That is going to be one heavy push 🙂
This is where passing components in props come in handy. Instead of passing to the Button the detailed express description of the Icon in form of its name and its props, our Button can simply say: "gimme an Icon, I don't intendance which 1, your selection, and I'll return it in the right identify".
Permit's encounter how it can be washed 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 exist precise, permit's build 3 buttons, with iii different APIs for passing the icon, and so compare them. Hopefully, it volition be obvious which one is amend in the stop. For the icon we're going to apply one of the icons from material ui components library. Lets start with the nuts and just build the API first.
Starting time: icon as React Element
We just need to pass an element to the icon prop of the button and and then render that icon well-nigh the children similar any other element.
i blazon ButtonProps = {
2 children : ReactNode ;
3 icon : ReactElement < IconProps > ;
4 } ;
5
half dozen export const ButtonWithIconElement = ( { children , icon } : ButtonProps ) => {
7 return (
viii < push button >
9
10
xi { icon }
12 { children }
13 </ button >
xiv ) ;
fifteen } ;
And then can use information technology like this:
ane < ButtonWithIconElement icon = { < AccessAlarmIconGoogle /> } > button here </ ButtonWithIconElement >
Second: icon every bit a Component
We need to create a prop that starts with a majuscule letter of the alphabet to signal it'due south a component, and then render that component from props like whatever other component.
one type ButtonProps = {
ii children : ReactNode ;
iii Icon : ComponentType < IconProps > ;
4 } ;
5
6 consign const ButtonWithIconComponent = ( { children , Icon } : ButtonProps ) => {
7 return (
8 < push >
9
10
11
12 < Icon />
13 { children }
14 </ button >
fifteen ) ;
16 } ;
And and then tin can use information technology similar this:
i import AccessAlarmIconGoogle from '@mui/icons-fabric/AccessAlarm' ;
ii
3 < ButtonWithIconComponent Icon = { AccessAlarmIconGoogle } > button hither </ ButtonWithIconComponent > ;
Tertiary: icon as a function
We demand to create a prop that starts with return to indicate it's a return office, i.eastward. a function that returns an element, phone call the office inside the button and add the result to component'due south render function as any other element.
1 type ButtonProps = {
2 children : ReactNode ;
three renderIcon : ( ) => ReactElement < IconProps > ;
4 } ;
five
six consign const ButtonWithIconRenderFunc = ( { children , renderIcon } : ButtonProps ) => {
7
8 const icon = renderIcon ( ) ;
nine return (
10 < button >
11
12 { icon }
13 { children }
14 </ button >
xv ) ;
16 } ;
And so employ it similar this:
ane < 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 instance in the codesandbox.
Time to put those APIs to a examination.
Modifying the size and colour of the icon
Permit'due south first run across whether we tin conform our icon according to our needs without disturbing the push button. Later on 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 merely pass some props to the icon. Nosotros are using material UI icons, they requite us fontSize and color for that.
ane < ButtonWithIconElement icon = { < AccessAlarmIconGoogle fontSize = " small " color = " warning " /> } > button here </ ButtonWithIconElement >
Second: icon as a Component
Also uncomplicated: nosotros need to excerpt our icon into a component, and pass the props in that location in the return chemical element.
1 const AccessAlarmIcon = ( ) => < AccessAlarmIconGoogle fontSize = " modest " color = " fault " /> ;
2
three const Page = ( ) => {
four return < ButtonWithIconComponent Icon = { AccessAlarmIcon } > push here </ ButtonWithIconComponent > ;
v } ;
Important: the AccessAlarmIcon component should always exist defined outside of the Folio component, otherwise it will re-create this component on every Folio re-render, and that is really bad for operation and prone to bugs. If you're not familiar with how quickly it tin turn ugly, this is the article for yous: How to write performant React code: rules, patterns, do's and don'ts
Third: icon as a Function
Near the aforementioned every bit the first i: only pass the props to the chemical element.
i < ButtonWithIconRenderFunc
2 renderIcon = { ( ) => (
3 < AccessAlarmIconGoogle fontSize = " minor " color = " success " />
4 ) }
5 >
Hands done for all three of them, nosotros have infinite flexibility to alter the Icon and didn't demand to bear on the button for a single thing. Compare information technology with iconName and iconProps from the very first case 🙂
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 probable than not, you'll accept some prop that control push's size equally well. Infinity flexibility is good, but for something as pattern systems, you'd want some pre-divers types of buttons. And for unlike buttons sizes, yous'd want the button to control the size of the icon, not go out it to the consumer, so yous won't end up with tiny icons in huge buttons or vice versa by accident.
Now information technology's getting interesting: is it possible for the button to control one aspect of an icon while leaving the flexibility intact?
Offset: icon as React Chemical element
For this one, it gets a picayune bit ugly. We receive our icon equally a pre-defined chemical element already, and then the only thing we can do is to clone that element past using React.cloneElement api and override some of its props:
1
2 const clonedIcon = React . cloneElement ( icon , { fontSize : 'small-scale' } ) ;
3
four return (
five < button >
6 { clonedIcon }
seven { children }
8 </ button >
9 ) ;
And at the consumer side we can just remove the fontSize belongings.
1 < ButtonWithIconElement icon = { < AccessAlarmIconGoogle colour = " warning " /> } />
Only what almost default value, not overriding? What if I want consumers to exist able to alter 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 equally default value:
1 const clonedIcon = React . cloneElement ( icon , {
two fontSize : icon . props . fontSize || 'small' ,
three } ) ;
From the consumer side everything stays as it was before
one < ButtonWithIconElement icon = { < AccessAlarmIconGoogle color = " alarm " fontSize = " big " /> } />
Second: icon as a Component
Fifty-fifty more interesting here. Starting time, nosotros demand to give the icon the default value on button side:
1 export const ButtonWithIconComponent = ( { children , Icon } : ButtonProps ) => {
2 return (
iii < push button >
iv < Icon fontSize = " small " />
five { children }
6 </ push >
7 ) ;
viii } ;
And this is going to work perfectly when we pass the directly imported icon:
ane import AccessAlarmIconGoogle from '@mui/icons-material/AccessAlarm' ;
2
3 < ButtonWithIconComponent Icon = { AccessAlarmIconGoogle } > button here </ ButtonWithIconComponent > ;
Icon prop is nothing more than than just a reference to fabric UI icon component here, and that i knows how to deal with those props. Merely we extracted this icon to a component when we had to pass to it some color, think?
1 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 whatever props. So our fontSize value from <Icon fontSize="small" /> from the button will be but swallowed. This whole pattern, if you've never worked with it before, can be disruptive, 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 demand to pass through the props that AccessAlarmIcon receives to the actual icon. Usually, information technology's done via spread:
1 const AccessAlarmIcon = ( props ) => < AccessAlarmIconGoogle { ... props } color = " error " /> ;
Or can be just paw-picked too:
1 const AccessAlarmIcon = ( props ) => < AccessAlarmIconGoogle fontSize = { props . fontSize } color = " error " /> ;
While this pattern seems complicated, information technology actually gives u.s.a. perfect flexibility: the button can easily gear up its own props, and the consumer can cull whether they want to follow the management buttons gives and how much of it they want, or whether they want to exercise their own thing. If, for case, I want to override push button's value and gear up my own icon size, all I need to practise is to ignore the prop that comes from the button:
i const AccessAlarmIcon = ( props ) => (
2
3
iv < AccessAlarmIconGoogle fontSize = " big " colour = " error " />
5 ) ;
3rd: icon equally a Part
This is going to be pretty much the same equally with icon as a Component, but with the function. Get-go, adapt the button to laissez passer settings to the renderIcon part:
i const icon = renderIcon ( {
ii fontSize : 'pocket-sized' ,
3 } ) ;
And then on the consumer side, similar to props in Component footstep, pass that setting to the rendered component:
1 < ButtonWithIconRenderFunc renderIcon = { ( settings ) => < AccessAlarmIconGoogle fontSize = { settings . fontSize } colour = " success " /> } >
2 button here
3 </ ButtonWithIconRenderFunc >
And once again, if we want to override the size, all nosotros demand to do is to ignore the setting and laissez passer our own value:
1 < ButtonWithIconRenderFunc
ii
3 renderIcon = { ( settings ) => < AccessAlarmIconGoogle fontSize = " big " color = " success " /> }
4 >
v push hither
half-dozen </ ButtonWithIconRenderFunc >
Encounter the codesandbox with all three examples.
Changing the icon when the push button is hovered
And now the final test that should decide everything: I want to give the ability for the users to alter the icon when the button is hovered.
First, permit's teach the button to notice the hover. Just some state and callbacks to gear up that state should do it:
1 export const ButtonWithIcon = ( ... ) => {
ii const [ isHovered , setIsHovered ] = useState ( fake ) ;
3
4 return (
5 < push button
vi onMouseOver = { ( ) => setIsHovered ( truthful ) }
vii onMouseOut = { ( ) => setIsHovered ( faux ) }
viii >
9 ...
10 </ button >
11 ) ;
12 } ;
And then the icons.
First: icon equally React Chemical element
That 1 is the most interesting of the bunch. First, we need to pass that isHover prop to the icon from the push:
1 const clonedIcon = React . cloneElement ( icon , {
2 fontSize : icon . props . fontSize || 'modest' ,
3 isHovered : isHovered ,
iv } ) ;
And at present, interestingly plenty, we created exactly the same mental circle that we had when we implemented "icon as Component". We passed isHover property to the icon component, at present we need to go to the consumer, wrap that original icon component into some other component, that component will accept isHover prop from the button, and it should return the icon nosotros want to render in the button. 🤯 If y'all managed to sympathise that explanation from but words I'll send you some chocolate 😅 Here's some lawmaking to make information technology easier.
Instead of the original unproblematic direct render of the icon:
1 < ButtonWithIconElement icon = { < AccessAlarmIconGoogle colour = " alarm " /> } > button here </ ButtonWithIconElement >
we should create a wrapper component that has isHovered in its props and renders that icons as a consequence:
1 const AlarmIconWithHoverForElement = ( props ) => {
2 return (
3 < AccessAlarmIconGoogle
4
5
6 { ... props }
7
viii colour = { props . isHovered ? 'primary' : 'warning' }
9 / >
ten ) ;
eleven } ;
And then return that new component in the button itself:
1 < ButtonWithIconElement icon = { < AlarmIconWithHoverForElement /> } > push button here </ ButtonWithIconElement >
Looks a niggling bit weird, just information technology works perfectly 🤷🏽♀️
Second: icon as a Component
Starting time, pass the isHover to the icon in the push button:
1 < Icon fontSize = " small " isHovered = { isHovered } />
And and so back to the consumer. And at present the funniest thing always. In the previous step we created exactly the same mental circle that we need to think when we're dealing with components passed as Components. And it's not only the mental motion picture of data flow, I can literally re-utilise exactly the same component from the previous step here! They are merely components with some props after all:
1 < ButtonWithIconComponent Icon = { AlarmIconWithHoverForElement } > button here </ ButtonWithIconComponent >
💥 works perfectly.
Tertiary: icon every bit a Part
Aforementioned story: only pass the isHovered value to the office as the arguments:
1 const icon = renderIcon ( {
2 fontSize : 'pocket-size' ,
3 isHovered : isHovered ,
4 } ) ;
And then use information technology on the consumer side:
i < ButtonWithIconRenderFunc
2 renderIcon = { ( settings ) => (
three < AccessAlarmIconGoogle
4 fontSize = { settings . fontSize }
5 color = { settings . isHovered ? "primary" : "warning" }
6 />
7 ) }
8 >
🎉 again, works perfectly.
Take a look at the sandbox with the working solution.
Summary and the respond: which manner is The Correct Mode™️?
If you read the full article, yous're probably saying right now: Nadia, aren't they are basically the aforementioned thing? What'due south the difference? You promised a clear reply, but I don't see it ☹️ And you lot're right.
And if you only scrolled hither correct away because y'all love spoilers: I'one thousand sorry, I lied a bit for the sake of the story 😳. There is no right reply hither.
All of them are more or less the same and y'all probably can implement 99% of the needed use cases (if non 100%) with just i pattern everywhere. The only deviation here is semantics, which area has the most complication, and personal preferences and religious beliefs.
If I had to extract some general rules of which pattern should be used where, I'd probably become with something similar this:
- I'd employ "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 equally a Component" blueprint (
<Button Icon={Icon} />) when I need to heavily alter and customise this component on the "receiving" side through its props, while at the same fourth dimension allowing users full flexibility to override those props themselves (pretty much as react-select does forcomponentsprop). - I'd apply "component as a Function" pattern (
<Push renderIcon={() => <Icon />} />) when I need the consumer to modify the result of this role, depending on some values coming from the "host" component itself (pretty much what Material UI Data Filigree component does withrenderCellprop)
Hope this article made those patterns easier to sympathize and now you tin can employ all of them when the use example needs it. Or yous can at present just totally ban whatever of them in your repo, simply for fun or consistency sake, since at present you tin can implement whatever you want with just 1 pattern 😊
Run across ya adjacent time! ✌🏼
Source: https://www.developerway.com/posts/react-component-as-prop-the-right-way
0 Response to "React Pass Props to Component Again"
Post a Comment