This site runs best with JavaScript enabled.

How to give rendering control to users with prop getters

October 02, 2017


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...

dog wagging the tail while waiting

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.

React toggled logo

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

1import {Component} from 'react'
2import PropTypes from 'prop-types'
3
4const callAll = (...fns) => (...args) => fns.forEach(fn => fn && fn(...args))
5
6class 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 * Takes an argument and if it's an array, returns the first item in the array
68 * otherwise returns the argument
69 * @param {*} arg the maybe-array
70 * @return {*} the arg or it's first item
71 */
72
73function unwrapArray(arg) {
74 return Array.isArray(arg) ? arg[0] : arg
75}
76
77export 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 togglerPropsoverrides 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:

1getTogglerProps = (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:

1const 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](https://github.com/paypal/downshift)🏎 - Primitive for building simple, flexible, WAI-ARIA compliant enhanced input React components
  • [react-toggled](https://github.com/kentcdodds/react-toggled) - Component to build simple, flexible, and accessible toggle components
  • [dub-step](https://github.com/infiniteluke/dub-step)πŸ•Ί - Step through an index with style
  • [react-stepper-primitive](https://github.com/ajoslin/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! πŸ‘

Things to not miss:

  • [import-all.macro](https://github.com/kentcdodds/import-all.macro) - A work in progress babel-macro that allows you to import all files that match a glob
  • [react-powerplug](https://github.com/renatorib/react-powerplug) - Pretty neat/interesting idea with the render prop pattern.
  • [graphql-tag.macro](https://github.com/leoasis/graphql-tag.macro) - Impressive babel-macro that precompiles graphql queries.
  • [size-limit](https://github.com/ai/size-limit) - Prevent JS libraries bloat. If you accidentally add a massive dependency, Size Limit will throw an error.
  • left-pad not found stickerβ€Šβ€”β€ŠSee below. This was created by my friend Tyler McGinnis (of Tyler McGinnis fame) and I think it’s pretty funny.

Discuss on Twitter β€’ Edit post on GitHub

Share article
Kent C. Dodds

Kent C. Dodds is a JavaScript software engineer and teacher. He's taught hundreds of thousands of people how to make the world a better place with quality software development tools and practices. He lives with his wife and four kids in Utah.

Learn more with Kent C. Dodds:

  • Learn React Hooks: Join me in this remote workshop. I'll teach you the ins and outs of React Hooks. Tickets are limited! 🎟
  • TestingJavaScript.com: Jump on this self-paced workshop and learn the smart, efficient way to test any JavaScript application. πŸ†