DEV Community

loading...

Using Hooks in Class Components with a Render Prop

zachsnoek profile image Zach Snoek ใƒป3 min read

Let's say one of your coworkers has created a super fancy hook to replace some old code and your job is to implement it in all the places that need to use it. That hook is implemented like this:

// Warning: We are using the classic and _contrived_ counter to demonstrate this pattern.

const useCounter = (initialCount = 0) => {
  const [count, setCount] = React.useState(initialCount);

  const incrementCount = () => setCount(count + 1);
  const decrementCount = () => setCount(count - 1);

  return { count, incrementCount, decrementCount };
};
Enter fullscreen mode Exit fullscreen mode

We can consume it in a functional component like this:

const CounterDisplay = () => {
    const { count, incrementCount, decrementCount } = useCounter();

    return (
      <div>
        {`Count is: ${count}`}
        <button onClick={incrementCount}>+</button>
        <button onClick={decrementCount}>-</button>
      </div>
    );
}
Enter fullscreen mode Exit fullscreen mode

This is great and all, but what if some of your codebase uses class components, where hooks can't be used? One option is to create a component that passes the hook to a class component via a render prop.

Simply put, the render prop pattern allows components to share code. A component has a prop that accepts a function that returns a React element, and calls that function instead of returning its own renderable value. The component with the render prop shares its data by passing one or more arguments to the called function.

Let's see how we can create a component that passes our useCounter hook to our class component with a render prop. Here's the class component that we want to use useCounter in, with the return values of the hook where we plan to use them:

class CounterDisplay extends React.Component {
  render() {

    return (
      <div>
        {count}
        <button onClick={incrementCount}>+</button>
        <button onClick={decrementCount}>-</button>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

First, we'll create a component called Counter that accepts the render prop. When we use this component later, we will pass a function to the render prop that returns CounterDisplay.

const Counter = ({ render }) => {
  return null;
}
Enter fullscreen mode Exit fullscreen mode

Note: We've literally named the render prop render, but the prop can be named whatever you like; "render prop" refers to the pattern of a render prop, not a specific prop name. children as a function is another commonly-used way to implement a render prop.

Again, render will accept a function that returns a React element, so instead of Counter implementing and returning one itself, we can just return the result of calling render:

const Counter = ({ render }) => {
    return render();
}
Enter fullscreen mode Exit fullscreen mode

Great! But we still need to pass the value of useCounter to the render function, because right now this component is useless. Since Counter is a functional component, we can use useCounter and then pass its value to render:

const Counter = ({ render }) => {
  const counter = useCounter();
  return render(counter);
};
Enter fullscreen mode Exit fullscreen mode

Now we need to modify CounterDisplay to accept the value that Counter will pass to it. We can do this by accepting the value through its props:

class CounterDisplay extends React.Component {
  render() {
    const { count, incrementCount, decrementCount } = this.props;

    return (
      <div>
        {count}
        <button onClick={incrementCount}>+</button>
        <button onClick={decrementCount}>-</button>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

To recap so far: We've created a component Counter that accepts a render prop. It calls the function passed to render and also passes the return value of useCounter to it. We've modified CounterDisplay to get the value from its props which will allow us to use the value as we would in a functional component.

We can now put Counter and CounterDisplay together. Since we know that Counter is going to pass counter to render, we can pass it through CounterDisplay's props:

const App = () => {
  return (
    <Counter
      render={(counter) => (
        <CounterDisplay {...counter} />
      )}
    />
  )
}
Enter fullscreen mode Exit fullscreen mode

Now your codebase can take advantage of the great counting features that useCounter has to offer, even in class components.

The next time you need to use a hook in a class component, consider using a render prop.

Discussion (0)

pic
Editor guide