Until React v16.8, when we wanted to reuse some logic in our apps, we had two choose between these two alternatives: 1) Higher-Order Components (HOC) and 2) Render Props. We have a new way now: hooks.

For most of my colleagues, there is no difference between HOCs and Render Props at all. But in the end, everyone needs to choose the most ergonomic pattern for themselves.

The Higher-Order Component pattern is very popular. Because so many libraries use it, React developers are also more used to it. It was quite easy to understand for me when I began working with React, and, frankly, it’s just handy. We can simply pass props with additional state management if needed.

In this article, I compare these two patterns and explain what changed with their application when hooks were released.

Here’s our example

Let’s say that we’re developing an app which is a simple counter. We want to implement two functions:

  1. We determine the type of this counter. In the first use case, the counter only views the result of incrementing and decrementing actions. In the second use case, the user is given an input where they can manually set the count, and then also increment, or decrement. In both cases, they can also reset the counter to zero.
  2. We also want our app to change control button colors when users set the counter state above 3.

To fulfill these needs, we need to pass two props to this Counter, type and color, in our main file.

To simplify our example, I’ve already extracted the Controller component where we have the buttons responsible for increment, decrement, and reset of our counter. The button color is passed as a prop from the counter component and it obviously depends on the current count. That’s why our counter component is a class component: we need the state to keep the count. 

We also have a Field component in our app. It receives the current value of input and the onChange handler to control this input.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import React, { Component, Fragment } from "react";
import Field from "../components/Field";
import Controller from "../components/Controller";

class Counter extends Component {
  state = { count: 0 };

  handleIncrement = () =>
    this.setState(prevState => ({ count: prevState.count + 1 }));

  handleDecrement = () =>
    this.setState(prevState => ({ count: prevState.count - 1 }));

  handleReset = () => this.setState({ count: 0 });

  handleChange = e => {
    const { value } = e.target;
    this.setState({ count: parseInt(value, 0) });
  };

  render() {
    const { count } = this.state;
    const { type, color } = this.props;

    return (
      <Fragment>
        {type === "input" ? (
          <Field value={count} onChange={e => this.handleChange(e)} />
        ) : (
          <p>Count: {count}</p>
        )}
        <Controller
          color={count < 3 ? color : "#A4031F"}
          onDecrement={this.handleDecrement}
          onIncrement={this.handleIncrement}
          onReset={this.handleReset}
        />
      </Fragment>
    );
  }
}

export default Counter;

Live demo: https://codesandbox.io/s/countersimple-osbf9

At this point, we can focus on the separation of concerns in further development.

That makes the Counter component more reusable and allows to separate the logic responsible for state management and rendering the component.


Higher-Order Component

So, let’s move the counter logic into a separate component and make it into a Higher-Order Component:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import React, { Component } from "react";

const withCounterLogic = EnhancedComponent => {
  return class extends Component {
    state = { count: 0 };

    handleIncrement = () =>
      this.setState(prevState => ({ count: prevState.count + 1 }));

    handleDecrement = () =>
      this.setState(prevState => ({ count: prevState.count - 1 }));

    handleReset = () => this.setState({ count: 0 });

    handleChange = e => {
      const { value } = e.target;
      this.setState({ count: parseInt(value, 0) });
    };

    render() {
      const { count } = this.state;
      const { type } = this.props;

      return (
        <EnhancedComponent
          type={type}
          count={count}
          handleIncrement={this.handleIncrement}
          handleDecrement={this.handleDecrement}
          handleReset={this.handleReset}
          handleChange={e => this.handleChange(e)}
          {...this.props}
        />
      );
    }
  };
};

export default withCounterLogic;

And our Counter will then look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React, { Fragment } from "react";
import Field from "../components/Field";
import Controller from "./Controller";
import withCounterLogic from "../withCounterLogic";

const Counter = ({
  type,
  color,
  count,
  handleChange,
  handleDecrement,
  handleIncrement,
  handleReset
}) => (
  <Fragment>
    {type === "input" ? (
      <Field value={count} onChange={e => handleChange(e)} />
    ) : (
      <p>Count: {count}</p>
    )}
    <Controller
      color={count < 3 ? color : "#A4031F"}
      onDecrement={handleDecrement}
      onIncrement={handleIncrement}
      onReset={handleReset}
    />
  </Fragment>
);

export default withCounterLogic(Counter);

Live demo: https://codesandbox.io/s/counterhoc-c5nz0

Now our code is far more readable. The Counter logic is separated from the UI. But this solution has its cons as well and can possibly lead to problems with:

– knowing where the state is coming from and how it’s handled,

– where all these props came from (in this pattern, we’re mixing props from the main file, and ones coming from our HOC).

Of course, it’s not always necessary to address these issues, but it’s good to be aware of them (especially if they end up causing bugs). In general, I think mixing two “kinds” of props can be confusing. And most of the time, it can be really hard to find that type of bug, because React won’t give us any error alert.


Render Props

One way to minimize that risk is using the Render Props pattern. Let’s refactor our initial code once again.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { Component, Fragment } from "react";

class CounterLogicWrapper extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      handleIncrement: this.handleIncrement,
      handleDecrement: this.handleDecrement,
      handleReset: this.handleReset,
      handleChange: e => this.handleChange(e)
    };
  }

  handleIncrement = () =>
    this.setState(prevState => ({ count: prevState.count + 1 }));

  handleDecrement = () =>
    this.setState(prevState => ({ count: prevState.count - 1 }));

  handleReset = () => this.setState({ count: 0 });

  handleChange = e => {
    const { value } = e.target;
    this.setState({ count: parseInt(value, 0) });
  };

  render() {
    return <fragment>{this.props.children(this.state)}</fragment>;
  }
}

export default CounterLogicWrapper;

And then transform Counter as well, like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { Fragment } from "react";
import Field from "../components/Field";
import Controller from "./Controller";

import CounterLogicWrapper from "../CounterLogicWrapper";

const Counter = ({ type, color }) => (
  <CounterLogicWrapper>
    {({
      count,
      handleChange,
      handleDecrement,
      handleIncrement,
      handleReset
    }) => (
      <Fragment>
        {type === "input" ? (
          <Field value={count} onChange={e => handleChange(e)} />
        ) : (
          <p>Count: {count}</p>
        )}
        <Controller
          color={count < 3 ? color : "#A4031F"}
          onDecrement={handleDecrement}
          onIncrement={handleIncrement}
          onReset={handleReset}
        />
      </Fragment>
    )}
  </CounterLogicWrapper>
);

export default Counter;

Live demo: https://codesandbox.io/s/counterrenderprops-sgcm1

We now have full control over our Counter component and the logic is separated. Both the count state and the handlers responsible for state management are passed through the children prop of the wrapper component.

Props from the main file (type and color) are taken from the actual component props, so they just can’t be mixed.

To sum up, using the Render Prop Pattern made our code much more predictable. In this case, we have a dynamic composition where we can implement any HOC with Render Prop pattern, but we can’t accomplish that the other way round. 

We can even implement a HOC with the Render Prop inside.

But note that both patterns come with these cons:

– They introduce additional levels of abstraction to React concepts.

– They’re sometimes unnecessarily complex, and that complexity should possibly be extracted.

And last but not least, when we have multiple wrappers, we can accidentally land in something called the wrapper hell.

Our component is less and less readable and nested a few times. So generally, it’s not that handy and can be really hard to debug.


Hooks

Then came hooks. And that’s when the magic happened. 

One of the key advantages of hooks is that they help us organize code inside the component with customizable pieces that we can extract and reuse in other places.

Let’s start with refactoring our first component. We can do that in two ways: by using the useState or useReducer hook. 

For simplicity, let’s use the useState hook. I’ve transformed a class component into a function component, and respectively changed our methods for updating the state.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React, { Fragment, useState } from "react";
import Field from "./Field";
import Controller from "./Controller";

const Counter = ({ type, color }) => {
  const [count, setCount] = useState(0);
  const handleIncrement = () => setCount(count + 1);
  const handleDecrement = () => setCount(count - 1);
  const handleReset = () => setCount(0);
  const handleChange = e => {
    const { value } = e.target;
    setCount(parseInt(value, 0));
  };

  return (
    <Fragment>
      {type === "input" ? (
        <Field value={count} onChange={e => handleChange(e)} />
      ) : (
        <p>Count: {count}</p>
      )}
      <Controller
        color={count < 3 ? color : "#A4031F"}
        onDecrement={handleDecrement}
        onIncrement={handleIncrement}
        onReset={handleReset}
      />
    </Fragment>
  );
};

export default Counter;

Live demo: https://codesandbox.io/s/counterusestate-4i8nf

After this kind of refactoring, we can create a custom hook that will extract the logic from our component completely.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import React, { Fragment, useState } from "react";
import Field from "./Field";
import Controller from "./Controller";

const useCounter = initialValue => {
  const [count, setCounter] = useState(initialValue);
  const handleIncrement = () => setCounter(count + 1);
  const handleDecrement = () => setCounter(count - 1);
  const handleReset = () => setCounter(0);
  const handleChange = e => {
    const { value } = e.target;
    setCounter(parseInt(value, 0));
  };

  return {
    count,
    handleIncrement,
    handleDecrement,
    handleReset,
    handleChange
  };
};

const Counter = ({ type, color }) => {
  const {
    count,
    handleIncrement,
    handleDecrement,
    handleReset,
    handleChange
  } = useCounter(0);

  return (
    <Fragment>
      {type === "input" ? (
        <Field value={count} onChange={e => handleChange(e)} />
      ) : (
        <p>Count: {count}</p>
      )}
      <Controller
        color={count < 3 ? color : "#A4031F"}
        onDecrement={handleDecrement}
        onIncrement={handleIncrement}
        onReset={handleReset}
      />
    </Fragment>
  );
};

export default Counter;

Live demo: https://codesandbox.io/s/countercustomhook-py1he

And that’s how we can enhance our previous components.


So how will our two components look right now?

Let’s implement our custom hook into the component with HOC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React from "react";
import useCounter from "./hooks";

const withCounterLogic = EnhancedComponent => {
  return ({ type, ...props }) => {
    const {
      count,
      handleIncrement,
      handleDecrement,
      handleReset,
      handleChange
    } = useCounter(0);

    return (
      <EnhancedComponent
        type={type}
        count={count}
        handleIncrement={handleIncrement}
        handleDecrement={handleDecrement}
        handleReset={handleReset}
        handleChange={e => handleChange(e)}
        {...props}
      />
    );
  };
};

export default withCounterLogic;

Live demo: https://codesandbox.io/s/counterhochook-kjwmt

And with Render Props:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React, { Fragment } from "react";
import useCounter from "./hooks";

const CounterLogicWrapper = props => {
  const {
    count,
    handleIncrement,
    handleDecrement,
    handleReset,
    handleChange
  } = useCounter(0);

  return (
    <fragment>
      {props.render({
        count,
        handleIncrement,
        handleDecrement,
        handleReset,
        handleChange: e => handleChange(e)
      })}
    </fragment>
  );
};

export default CounterLogicWrapper;

Live demo: https://codesandbox.io/s/counterrenderpropsHooks-h4hlh

As you can see, our logic is even more reusable than before. But still, we can use just the useCounter custom hook alone, or with the HOC or Render Props pattern (in more specific use cases).


Wrapping up

Many people believe that hooks might be the end of the patterns mentioned above. Personally, I’m a fan of a more peaceful approach. I think that all these patterns can coexist and more importantly, with hooks we get proper tools to use HOC’s or Render Props more efficiently.

Natalia Kulas
Natalia
Frontend Engineer

Natalia is a fronted developer at Sunscrapers. She adores React primarily for its rebellious nature. A cat lover, she’s passionate about psychology and always willing to level up her mindset.

Data science Web development

5 projects that will benefit from Python

One look at the TIOBE index is enough to see that Python is one of the most popular programming languages today. And no wonder: Python is simple, versatile, and [...]

Data science Web development

Single Responsibility Principle in React applications – Part 1

In my work as a front-end developer, I often encounter single methods, classes, and React components doing way too much. These blocks of code are typically long, use many [...]

Join our newsletter.

Scroll to bottom

Hi there, we use cookies to provide you with an amazing experience on our site. If you continue without changing the settings, we'll assume that you're happy to receive all cookies on the Sunscrapers website. You can change your cookie settings at any time.

Learn more