DEV Community

Anton Gunnarsson
Anton Gunnarsson

Posted on • Edited on • Originally published at antongunnarsson.com

Render props in the Age of Hooks

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.

You can read the original, interactive version of this post here.

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!

If you know what render props are but are curious about its current use cases, feel free to skip ahead!

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:

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

  return (
    <div>
      <h1>Counter</h1>
      <button onClick={() => setCount(c => c + 1)}>Increase count</button>
      <p>{count}</p>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>Counter</h1>
      <button onClick={() => setCount(c => c + 1)}>Increase count</button>
      <p>{renderCount(count)}</p>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

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

And here is how we now use this component:

<Counter renderCount={count => <span>The count is {count}!</span>} />
Enter fullscreen mode Exit fullscreen mode

We pass in the renderCount prop as an arrow function that receives the count and returns a span with our desired text.

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

Note that the prop renderCount could be called anything.

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>Counter</h1>
      <button onClick={() => setCount(c => c + 1)}>Increase count</button>
      <p>{children(count)}</p>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

In reality, it's more likely you'll encounter components that use render props to handle tracking mouse movement, scrolling or similar things.

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>
Enter fullscreen mode Exit fullscreen mode

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} />
)
Enter fullscreen mode Exit fullscreen mode

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())
}
Enter fullscreen mode Exit fullscreen mode

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>
    )
  }
}
Enter fullscreen mode Exit fullscreen mode

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} />
Enter fullscreen mode Exit fullscreen mode

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>}
/>
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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! πŸ™Œ

Top comments (0)