ag

Render props in the Age of Hooks

šŸŒø This post has bloomed and is unlikely to change.

Through the years many different patterns have emerged to solve problems we encounter writing React components. One of the most popular patterns ever is the render prop-pattern.

In this post, weā€™ll walk through what render props are, what the implementation looks like, and how they fit into the React landscape now that we live in the Golden Age of Hooks. Letā€™s get started!

So what is a render prop?

In theory, a render prop is a way to share common functionality. It follows a principle called ā€œInversion of Controlā€ which is a way to move control from the abstraction to the user of said abstraction.

Wait.. what?

Yeah, I know. Letā€™s take a look at a very simplified example instead of talking theory.

This is a small component that renders a button and when you click that button you increase the count by one.

Click it, I'm interactive!

0

The implementation looks like this:

export default function Counter() {
  const [count, setCount] = useState(0)

  return (
    <div>
      <h1>Click it, I'm interactive!</h1>
      <button onClick={() => setCount((c) => c + 1)}>Increase count</button>
      <p>{count}</p>
    </div>
  )
}

Now, for the sake of the example letā€™s say we want to give the user more control of how the number is displayed. The first thought might be to add a prop to the component to add some styling. That would work if we just wanna change the styling, but what if we run into a situation where we also want to wrap the count in some text? While we could add another prop for this itā€™s also a perfect time to try using a render prop.

Imagining we want to add some styling and then display the count like ā€The count is X!ā€ we can move this control to the consumer of the component by refactoring our component to this:

export default function Counter({ renderCount }) {  const [count, setCount] = useState(0)

  return (
    <div>
      <h1>Click it, I'm interactive!</h1>
      <button onClick={() => setCount(c => c + 1)}>Increase count</button>
      <p>{renderCount(count)}</p>    </div>
  )
}

Now our component receives a prop called renderCount which we expect to be a function. We then invoke this function on line 8 passing it the current count.

And here is how we now use this component:

<Counter renderCount={(count) => <span>The count is {count}!</span>} />

We pass in the renderCount prop as an arrow function that receives the count and returns a span with our desired text. (Note that the prop renderCount could be called anything.)

And now our component looks like this:

Click it, I'm interactive!

The count is 0!

By doing this weā€™ve inverted the control of rendering the count from the component itself to the consumer of the component.

Function as children

Before moving on to why render props arenā€™t as widely used anymore and in which cases they might still be relevant, I just want to mention the concept of function as children. While React doesnā€™t support passing a function as a child of a component and rendering it, you can use it together with render props since children are just a prop.

Refactoring our component once again we end up with this:

export default function Counter({ children }) {  const [count, setCount] = useState(0)

  return (
    <div>
      <h1>Click it, I'm interactive!</h1>
      <button onClick={() => setCount(c => c + 1)}>Increase count</button>
      <p>{children(count)}</p>    </div>
  )
}

This looks very similar to before, weā€™ve just removed our custom prop and now uses the reserved children prop instead and hence we pass the function down as the child:

<Counter>{(count) => <span>The count is {count}!</span>}</Counter>

I had a really hard time wrapping my head around this syntax when I first learned about render props, but itā€™s also the most popular way of using them so itā€™s likely youā€™ll encounter it too.

Drawbacks

While render props might sound great there are a couple of issues that I want to talk about.

One problem is that when you get to a point where you need to use multiple components with a render prop api you could end up in what you might recognize as ā€œthe pyramid of deathā€. Below is an example where we have a component that needs access to its measured size, the scroll position, mouse position, and some styling for animation purposes:

<Mouse>
  {(mouse) => (
    <Scroll>
      {(scroll) => (
        <Motion>
          {(style) => (
            <Measure>
              {(size) => (
                <ConsumingComponent mouse={mouse} scroll={scroll} style={style} size={size}></ConsumingComponent>
              )}
            </Measure>
          )}
        </Motion>
      )}
    </Scroll>
  )}
</Mouse>

Comparing this to a pseudo-code version using Hooks you can see why a lot of people prefer Hooks:

const mouse = useMouse()
const scroll = useScroll()
const style = useMotion()
const size = useMeasure()

return <ConsumingComponent mouse={mouse} scroll={scroll} style={style} size={size} />

Another thing that this example illustrates is that we get a much clearer separation between rendering and preparing to render. In the render prop-example we donā€™t care about the <Mouse> component, we just care about the value we get in the render prop function.

This also means that if need to use or process the values returned by our hooks we donā€™t need to have this logic mixed with what we return. This separation is so much clearer in comparison to render props which I think is very good.

In the Age of Hooks

When Hooks were introduced back in 2018 I canā€™t say that the community screamed with joy. The reaction was mostly complaining about this completely new thing that weā€™ll also have to learn. Yet here we are two years later and most of the hate has died down and modern React is now mostly defined by Hooks. This has also meant that the render prop pattern isnā€™t as popular as it was just a couple of years ago. But while hooks are superior to render props in most cases there are still a couple of situations where you might want to reach for a good olā€™ render prop.

Wrapping hooks

One of the most straight forward use cases for render props is to wrap hooks so you can use them in class components. Letā€™s say weā€™ve previously used a render prop to track whether the mouse is hovering an element and now we refactor this to a useHover hook instead. To use this in a class component we can wrap it in a render prop:

function Hover({ children }) {
  return children(useHover())
}

And then we can use it in a class component just like we would if Hover took care of the implementation itself:

class MyComponent extends React.Component {
  render() {
    return (
      <Hover>
        {([hoverRef, isHovered]) => {
          return <div ref={hoverRef}>{isHovered ? 'šŸ˜ƒ' : 'šŸ˜ž'}</div>
        }}
      </Hover>
    )
  }
}

Pretty neat, right?

Enable custom rendering

In the below example we have a component called Grid that takes a prop called data. It renders a table with two rows and two columns, and handles the logic for sorting, filtering, rearranging columns and so on.

const data = [
  {
    name: 'Anton',
    age: 28,
  },
  {
    name: 'Nisse',
    age: 32,
  },
]

return <Grid data={data} />

Now imagine that we need to change how a row or cell is displayed. This is a perfect opportunity to implement two render props in the component that defers this rendering to the user:

<Grid data={data} rowRenderer={(row, idx) => <div>...</div>} cellRenderer={(cell, row, idx) => <div>...</div>} />

This could be implemented with a hook that takes the renderers as arguments, but in this case I believe the render prop api is much more pleasant to work with.

Performance

Finally, I recently watched a talk by @erikras and learned about a third use case when you might want to use render props. Below is a component that uses the previously mentioned useHover hook, but it also renders a component called VerySlowToRender which is, well.. very slow to render. Itā€™s probably from a third-party package that you have no control over but for some reason you still have to use it.

function MyComponent() {
  const [hoverRef, isHovered] = useHover()

  return (
    <VerySlowToRender>      <div ref={hoverRef}>{isHovered ? 'šŸ˜ƒ' : 'šŸ˜ž'}</div>
    </VerySlowToRender>  )
}

So in this case the problem is that when you hover the div the entire component will rerender, including the slow part. One way to solve this might be to try wrapping the slow component in some memoization or breaking out the div that is being hovered into its own component, but sometime that might feel like overkill.

What we could do instead is to use our previously defined Hover component with a render prop!

function MyComponent() {
  return (
    <VerySlowToRender>
      <Hover>        {([hoverRef, isHovered]) => {          return <div ref={hoverRef}>{isHovered ? 'šŸ˜ƒ' : 'šŸ˜ž'}</div>        }}      </Hover>    </VerySlowToRender>
  )
}

Now when we hover the only thing thatā€™s gonna rerender is the div! I do think this is maybe the most opinionated use of the render prop pattern, and I canā€™t decide whether I prefer this over breaking it out to another component. Choice is always good though!

Summary

While Hooks have taken over a lot of the responsibility of render props, render props should still have a seat at the table of patterns we use when solving problems with React, as long as we use them for the right reasons.

Thanks for reading! šŸ™Œ

DEV.to badgeDiscuss on DEV

WebMentions

What's this?
Nothing's here yet! Tweet about this post to show up here.