Demystifying Higher-Order Components (HOCs)

Demystifying Higher-Order Components (HOCs)

·

16 min read

The term, ‘Higher-Order components’(usually abbreviated as ‘HOCs’) gets tossed around a lot, and people often tend to misunderstand it. In fact, a good number of React developers don’t exactly know what HOCs are or how they work. And that’s okay. Even the official React documentation covers it under the ‘advanced guides’ section.

But is it really that difficult to grasp?

Heck, no.

Matter of fact, chances are you’re already using HOCs in your codebase; you just don’t know it yet.

Let me show you!

But first, what are HOCs?

A HOC is simply a function that takes in a component as an argument and returns an upgraded/enhanced version of it.

imgonline-com-ua-twotoone-9kDMugSaiX4Ku.jpg

You dig?

Why(and how) do we use them?

Glad you asked!

I’ll walk you through. Let’s begin by bootstrapping our app using create-react-app. Run the following command in your terminal:

npx create-react-app hoc-example

Now, let’s open the folder in our text editor(I’m using VS code). Create a folder named ‘components’ under your ‘src’ folder, like so:

folder-structure.PNG

Everything looking nice and sleek? Great! Let’s march on.

Consider the following scenario: We want to render a header to the screen and have the color change whenever we hover over it.

Create a new component under the ‘components’ folder. Let’s call it component1.jsx

//Component1.jsx
import React, { Component } from "react";

class Component1 extends Component {
  constructor(props) {
    super(props);
    this.state = { color: "black" };
  }

  changeColor = () => {
    this.setState({ color: "pink" });
  };

  restoreColor = () => {
    this.setState({ color: "black" });
  };

  render() {
    const { color } = this.state;
    return (
      <h1
        style={{ color }}
        onMouseEnter={this.changeColor}
        onMouseLeave={this.restoreColor}
      >
        Component 1
      </h1>
    );
  }
}

export default Component1;

You’d see that we defined functions(changeColor and restoreColor) and we attached them to events that occur on the header. Hovering over the header will change the color to pink and taking my cursor out of the header will restore it to its default color(black).

Now, let’s add it to our App(top-level) component:

//App.js
import React from "react";
import Component1 from "./components/component1.jsx";
import "./App.css";

class App extends React.Component {
  render() {
    return (
      <div className="App">
        <Component1 />
      </div>
    );
  }
}

export default App;

When we run npm start in our terminal, we should see something like this in our browser

component1.gif

Great!

Now, we want to create another component; it should render a paragraph this time. We also want to change the color to pink when the user presses a mouse button over it and restore the color to default(black) when the user releases the mouse button over it. Let’s call it component2.jsx.


//Component2.jsx
import React, { Component } from "react";

class Component2 extends Component {
  constructor(props) {
    super(props);
    this.state = { color: "black" };
  }

  changeColor = () => {
    this.setState({ color: "pink" });
  };

  restoreColor = () => {
    this.setState({ color: "black" });
  };

  render() {
    const { color } = this.state;
    return (
      <p
        style={{ color }}
        onMouseDown={this.changeColor}
        onMouseUp={this.restoreColor}
      >
        Lorem Ipsum is simply dummy text of the printing and typesetting
        industry. Lorem Ipsum has been the industry standard dummy text ever
        since the 1500s, when an unknown printer took a galley of type and
        scrambled it to make a type specimen book. It has survived not only five
        centuries, but also the leap into electronic typesetting, remaining
        essentially unchanged. It was popularised in the 1960s with the release
        of Letraset sheets containing Lorem Ipsum passages, and more recently
        with desktop publishing software like Aldus PageMaker including versions
        of Lorem Ipsum.
      </p>
    );
  }
}

export default Component2;

Now, import this in the App(top-level) component

//App.js
import React from "react";
import Component1 from "./components/component1.jsx";
import Component2 from "./components/component2.jsx";
import "./App.css";

class App extends React.Component {
  render() {
    return (
      <div className="App">
        <Component1 />
        <Component2 />
      </div>
    );
  }
}

export default App;

This is the result:

withcomponent2.gif

It works! Great.

But is it really?

Let’s find out.

Now, say we want to create yet another component; it should render a button that changes it’s color to pink(yes, I do love pink; thanks for asking) when clicked.

You’ll start to notice that we’re repeating the exact, same logic(hence, writing the exact, same code) in a tonne of different components.

wellthatsnotgood.gif

Remember DRY?

Yeah, that software engineering principle that says “Do not Repeat Yourself”.

This goes completely against it.

So what’s an easy solution to this?

HOCs…durrh (I mean, that’s what this article is about)

Let’s create our very own Higher-Order Component, shall we?

Whip up a new component, real quick. It’s going to be returned in a function this time. We’ll call the function withMouseActions. Examine the following code:

//withMouseActions.jsx
import React from "react";

//HIGHER-ORDER COMPONENT
const withMouseActions = WrappedComponent => {
  class NewComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        color: "black"
      };
    }

    changeColor = () => {
      this.setState({ color: "pink" });
    };

    restoreColor = () => {
      this.setState({ color: "black" });
    };

    render() {
      return (
        <WrappedComponent
          color={this.state.color}
          changeColor={this.changeColor}
          restoreColor={this.restoreColor}
        />
      );
    }
  }
  return NewComponent;
};

export default withMouseActions;

As you can see, all the repeated logic has been moved to this HOC and passed as props to the wrapped component. We can now import this HOC in our initial components and delete the redundant code.

Our initial components should now look something like this

//Component1.jsx
import React, { Component } from "react";
import withMouseActions from "./withMouseActions.jsx";

class Component1 extends Component {
  render() {
    const { color, changeColor, restoreColor } = this.props;
    return (
      <h1
        style={{ color }}
        onMouseEnter={changeColor}
        onMouseLeave={restoreColor}
      >
        Component 1
      </h1>
    );
  }
}

// Wrapping the component inside the HOC to give it access to the logic(as props)
export default withMouseActions(Component1);
//Component2.jsx
import React, { Component } from "react";
import withMouseActions from "./withMouseActions.jsx";

class Component2 extends Component {
  render() {
    const { color, changeColor, restoreColor } = this.props;
    return (
      <p style={{ color }} onMouseDown={changeColor} onMouseUp={restoreColor}>
        Lorem Ipsum is simply dummy text of the printing and typesetting
        industry. Lorem Ipsum has been the industry's standard dummy text ever
        since the 1500s, when an unknown printer took a galley of type and
        scrambled it to make a type specimen book. It has survived not only five
        centuries, but also the leap into electronic typesetting, remaining
        essentially unchanged. It was popularised in the 1960s with the release
        of Letraset sheets containing Lorem Ipsum passages, and more recently
        with desktop publishing software like Aldus PageMaker including versions
        of Lorem Ipsum.
      </p>
    );
  }
}

// Wrapping the component inside the HOC to give it access to the logic(as props)
export default withMouseActions(Component2);

A lot cleaner, eh?

Benefit 1: code reusability, as opposed to code repetition.

Now, we can go a step further to simplify our components by turning them into pure, functional components.

//Component1.jsx
import React from "react";
import withMouseActions from "./withMouseActions.jsx";

const Component1 = ({ color, changeColor, restoreColor }) => (
  <h1 style={{ color }} onMouseEnter={changeColor} onMouseLeave={restoreColor}>
    Component 1
  </h1>
);

// Wrapping the component inside the HOC to give it access to the logic(as props)
export default withMouseActions(Component1);
//Component2.jsx
import React from "react";
import withMouseActions from "./withMouseActions.jsx";

const Component2 = ({ color, changeColor, restoreColor }) => (
  <p style={{ color }} onMouseDown={changeColor} onMouseUp={restoreColor}>
    Lorem Ipsum is simply dummy text of the printing and typesetting industry.
    Lorem Ipsum has been the industry's standard dummy text ever since the
    1500s, when an unknown printer took a galley of type and scrambled it to
    make a type specimen book. It has survived not only five centuries, but also
    the leap into electronic typesetting, remaining essentially unchanged. It
    was popularised in the 1960s with the release of Letraset sheets containing
    Lorem Ipsum passages, and more recently with desktop publishing software
    like Aldus PageMaker including versions of Lorem Ipsum.
  </p>
);

// Wrapping the component inside the HOC to give it access to the logic(as props)
export default withMouseActions(Component2);

Even cleaner! And they’re functional components now, which means easier testability, increased readability, improved performance and decreased coupling; benefits 2,3,4 and 5 😎.

hi 5.gif

Some of the popular HOCs you’re probably already using without being aware include react-router-dom’s withRouter and react-redux’s connect. Told ya.

Note: Don’t forget to always add {…this.props} in your HOC to pass down the wrapped component’s original props to the new component. See WrappedComponent in the following snippet:

//withMouseActions.jsx
import React from "react";

//HIGHER-ORDER COMPONENT
const withMouseActions = WrappedComponent => {
  class NewComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        color: "black"
      };
    }

    changeColor = () => {
      this.setState({ color: "pink" });
    };

    restoreColor = () => {
      this.setState({ color: "black" });
    };

    render() {
      return (
        <WrappedComponent
          color={this.state.color}
          changeColor={this.changeColor}
          restoreColor={this.restoreColor}
          {...this.props} //WRAPPED COMPONENT'S ORIGINAL PROPS
        />
      );
    }
  }
  return NewComponent;
};

export default withMouseActions;

Everything looks good!

Now you might be thinking, “Why didn’t we just reuse the logic by lifting state? i.e. putting our logic(state and methods) in the closest common ancestor component, which, in this case, is our App(top-level) component, and passing it down as props”.

That could work.

See diagram below:

Flux tree(1).PNG

The red node represents the common ancestor to which logic has been lifted, the blue nodes represent the components that need to make use of that logic, and the common ancestor passes logic down through props.

However, what if we had a more complex structure? See diagram below:

Flux tree.PNG

Again, the red node represents the common ancestor to which logic has been lifted, the blue nodes represent the components that need to make use of that logic, the yellow nodes represent intermediary components that just so happen to be between the common ancestor and the base component. The white node is sipping tea and minding its business.

These yellow components have to receive logic(in form of props) from the red component, that they’re not in any way making use of.

In very large apps, you’d have to pass unsolicited props through a long chain of components just for them to get to the ones that actually need them.

That’s not very efficient, is it?

That’s why the HOC pattern is best fitted for achieving code reusability.

By using HOCs, we can have our logic in a single place and reuse it throughout the app, without making our components accept props they don’t care about.

Another cool thing we can do is pass in other arguments to the HOCs. Say I wanted to change the default color in the components from black to whatever, I could just pass in the color as an argument to the HOC.

//Component1.jsx
import React from "react";
import withMouseActions from "./withMouseActions.jsx";

const Component1 = ({ color, changeColor, restoreColor }) => (
  <h1 style={{ color }} onMouseEnter={changeColor} onMouseLeave={restoreColor}>
    Component 1
  </h1>
);

// Wrapping the component inside the HOC to give it access to the logic(as props)
export default withMouseActions(Component1, "blue");
//Component2.jsx
import React from "react";
import withMouseActions from "./withMouseActions.jsx";

const Component2 = ({ color, changeColor, restoreColor }) => (
  <p style={{ color }} onMouseDown={changeColor} onMouseUp={restoreColor}>
    Lorem Ipsum is simply dummy text of the printing and typesetting industry.
    Lorem Ipsum has been the industry's standard dummy text ever since the
    1500s, when an unknown printer took a galley of type and scrambled it to
    make a type specimen book. It has survived not only five centuries, but also
    the leap into electronic typesetting, remaining essentially unchanged. It
    was popularised in the 1960s with the release of Letraset sheets containing
    Lorem Ipsum passages, and more recently with desktop publishing software
    like Aldus PageMaker including versions of Lorem Ipsum.
  </p>
);

// Wrapping the component inside the HOC to give it access to the logic(as props)
export default withMouseActions(Component2, "green");
//withMouseActions.jsx
import React from "react";

//HIGHER-ORDER COMPONENT
const withMouseActions = (WrappedComponent, color) => {
  class NewComponent extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        color: color
      };
    }

    changeColor = () => {
      this.setState({ color: "pink" });
    };

    restoreColor = () => {
      this.setState({ color: color });
    };

    render() {
      return (
        <WrappedComponent
          color={this.state.color}
          changeColor={this.changeColor}
          restoreColor={this.restoreColor}
          {...this.props}
        />
      );
    }
  }
  return NewComponent;
};

export default withMouseActions;

The result:

record_000006.gif

And it’s a wrap

Read more about it here:

Here’s a link to the repo containing the complete code.

Also, feel free to ask your questions below or tweet at me. I’ll respond as soon as I can.