How to give rendering control to users with prop getters

Photo by Annie Spratt on Unsplash

Render props are awesome, put it together with prop getters and you have an awesome combination to give users of your React components...

If you're an egghead.io subscriber, you can also learn about this pattern with these two lessons: Use Prop Collections with Render Props and Use Prop Getters with Render Props

Since I released downshift 🏎 a few weeks ago. Of all things, I think the most common question I've gotten has been about the "prop getters." As far as I know, downshift is the first library to implement this pattern, so I thought I'd explain why it's useful and how to implement it. If you're unfamiliar with downshift, please read the intro post before you continue. Don't worry, I'll wait...

So, to recap from what you read, prop getters are one piece to the puzzle to let you hand rendering over to the users of your components (a great idea). I got the idea from Jared Forsyth one day at an airport. You can only really use it with the render prop pattern. It's basically a function which will return props when called and people must apply those props to the right element to hook together all the relevant elements to make the overarching component. Hopefully that's clear 😀

To talk about this, we'll actually use a different component I wrote recently that uses this pattern called react-toggled .

It's pretty small, so I'm just going to paste all of it here for you (see the syntax highlighted file here):

1 import { Component } from 'react' 2 import PropTypes from 'prop-types' 3 4 const callAll = ( ... fns ) => ( ... args ) => fns . forEach ( fn => fn && fn ( ... args ) ) 5 6 class Toggle extends Component { 7 static propTypes = { 8 defaultOn : PropTypes . bool , 9 on : PropTypes . bool , 10 onToggle : PropTypes . func , 11 children : PropTypes . oneOfType ( [ PropTypes . func , PropTypes . array ] ) . isRequired , 12 } 13 static defaultProps = { 14 defaultOn : false , 15 onToggle : ( ) => { } , 16 } 17 state = { 18 on : this . getOn ( { on : this . props . defaultOn } ) , 19 } 20 21 getOn ( state = this . state ) { 22 return this . isOnControlled ( ) ? this . props . on : state . on 23 } 24 25 isOnControlled ( ) { 26 return this . props . on !== undefined 27 } 28 29 getTogglerProps = ( props = { } ) => ( { 30 'aria-controls' : 'target' , 31 'aria-expanded' : Boolean ( this . getOn ( ) ) , 32 ... props , 33 onClick : callAll ( props . onClick , this . toggle ) , 34 } ) 35 36 getTogglerStateAndHelpers ( ) { 37 return { 38 on : this . getOn ( ) , 39 getTogglerProps : this . getTogglerProps , 40 setOn : this . setOn , 41 setOff : this . setOff , 42 toggle : this . toggle , 43 } 44 } 45 46 setOnState = ( state = ! this . getOn ( ) ) => { 47 if ( this . isOnControlled ( ) ) { 48 this . props . onToggle ( state , this . getTogglerStateAndHelpers ( ) ) 49 } else { 50 this . setState ( { on : state } , ( ) => { 51 this . props . onToggle ( this . getOn ( ) , this . getTogglerStateAndHelpers ( ) ) 52 } ) 53 } 54 } 55 56 setOn = this . setOnState . bind ( this , true ) 57 setOff = this . setOnState . bind ( this , false ) 58 toggle = this . setOnState . bind ( this , undefined ) 59 60 render ( ) { 61 const renderProp = unwrapArray ( this . props . children ) 62 return renderProp ( this . getTogglerStateAndHelpers ( ) ) 63 } 64 } 65 66 67 68 69 70 71 72 73 function unwrapArray ( arg ) { 74 return Array . isArray ( arg ) ? arg [ 0 ] : arg 75 } 76 77 export default Toggle

You'll notice that this.props.children is unwrapped, this is for preact compatibility.

And here's how you could use react-toggled :

1 < Toggle > 2 { ( { on , getTogglerProps } ) => ( 3 < div > 4 < button 5 { ... getTogglerProps ( { 6 onClick ( ) { 7 alert ( 'you clicked!' ) 8 } , 9 } ) } 10 > 11 Toggle me 12 </ button > 13 < div > { on ? 'Toggled On' : 'Toggled Off' } </ div > 14 </ div > 15 ) } 16 </ Toggle >

There are a few neat things about this component I may talk about in a future post, but for now, let's focus on the getTogglerProps function (that's the prop getter).

The cool thing about this pattern is that it allows users to render whatever they want. So your components take care of the hard and generic part (the logic of the component) and the user can take care of the easy and less-generic part: what to show and how it's styled given the state of the component.

So if users want the <div> to appear above the <button> or to not appear at all, then the user can simply do that without having to look up any docs for props or anything. This is pretty powerful!

With that said, the biggest question I get from folks about "prop getters" is:

Why are you using a function to get props? Why not just pass a regular object to my render callback and let me spread that instead of having to call a function?

What people are saying is they'd prefer to do: <button {...togglerProps} {...myOwnProps} /> rather than <button {...getTogglerProps(myOwnProps)} /> . I can understand why folks might prefer that. It feels like you have more control that way. However, we're actually doing something useful with this function and the props that you provide...

For this component, we care about the onClick prop you apply to your <button> . We need to call this.toggle . But what if you (as a user of the component) also wanted to have a handler for onClick ? You might try to write it like this: <button onClick={this.handleClick} {...togglerProps} /> . But you'd find that togglerProps overrides your custom onClick handler, so you could switch it to: <button {...togglerProps} onClick={this.handleClick} /> and now you have the opposite problem! Your custom onClick is overriding the onClick from togglerProps , so react-toggled isn't working at all.

With that context, let's see how we avoid this problem by using a function. Check out the implementation of getTogglerProps :

1 getTogglerProps = ( props = { } ) => ( { 2 'aria-controls' : 'target' , 3 'aria-expanded' : Boolean ( this . getOn ( ) ) , 4 ... props , 5 onClick : callAll ( props . onClick , this . toggle ) , 6 } )

You'll notice that the onClick prop is assigned to callAll(props.onClick, this.toggle) . The callAll function is pretty simple:

1 const callAll = ( ... fns ) => ( ... args ) => fns . forEach ( fn => fn && fn ( ... args ) )

It does what it says. Calls all the functions it's given, if they exist. In our case, both of our onClick handlers will be called as we need. (See the transpiled version if you're less accustomed to arrow functions).

To summarize, prop getters are one of the patterns that enable you to hand rendering responsibility to the user of your components (a really awesome idea). You can only really implement it with the render prop pattern (in our case we use the children prop, but you could use a render prop if you prefer).

Here are a few projects that implement the prop getters pattern:

downshift 🏎 - Primitive for building simple, flexible, WAI-ARIA compliant enhanced input React components

🏎 - Primitive for building simple, flexible, WAI-ARIA compliant enhanced input React components react-toggled - Component to build simple, flexible, and accessible toggle components

- Component to build simple, flexible, and accessible toggle components dub-step 🕺 - Step through an index with style

🕺 - Step through an index with style react-stepper-primitive - React primitives for a "stepper" component.

I hope to see more folks doing stuff like this in the future! Good luck to you all! 👍