Higher-Order Components in React

React emphasizes composition over inheritance, making it simple to compose larger components by reusing smaller ones. This approach is at odds with the way many programmers are accustomed to reusing code: inheritance Object-Oriented Programming (OOP).

React uses higher-order components (HoCs) as an alternative to inheritance. Similar to higher-order functions, which take or return other functions, HoCs are functions that take a component as an argument and return another component.

You’ve Probably Seen Higher-Order Components Already

If you’ve used react-redux, you’ve probably already used at least one higher-order component. One of its primary functions is connect, which accepts a component and returns a component connected to the Redux store, wrapping the one provided. It saves you the hassle of managing the logic connection to the store in multiple places in your app.

The Problem

When subcomponents have varying behavior, it is common to pass this specialized behavior through props. Let’s say you have a component like the following:


class BaseComponent extends React.Component {
  render() {
    return (
      <div onClick={this.props.onClick}
          style={this.props.style}>

        <h1>{ this.props.title }</h1>
        <p>{ this.props.content }</p>
      </div>
    );
  }
}

We could pass this an onClick event with the following jsx:


class ClickLogger extends React.Component {
  constructor(props) {
    super(props);

    this.onClick = this.onClick.bind(this);
  }

  onClick(e) {
    console.log(e)
  }

  render() {
    return (
      <div>
        <BaseComponent {...this.props} onClick={this.onClick} />
      </div>
    );
  }
}

This is a pretty standard pattern in React. However, let’s say you expand onClick to be a bit more complicated, maybe accessing state, and you want to pass it through props to a bunch of different components. You could, of course, create a lot of duplicated code, replicating ClickLogger to wrap each component where you want to pass that click handler. It’s not very DRY, though.

Creating a Higher-Order Component

If we want to inject the click handler into arbitrary components without redundancy, an HoC is perfect. The following code is a factory function that creates a higher-order component.


const withLogger = (WrappedComponent) => {
  return class ClickLogger extends React.Component {
    constructor(props) {
      super(props);

      this.onClick = this.onClick.bind(this);
    }

    onClick(e) {
      console.log(e)
    }

    render() {
      const { title, content } = this.props;
      return (
        <div>
          <WrappedComponent {...this.props} onClick={this.onClick} />
        </div>
      );
    }
  }
}

Now we can create a component that calls the click handler any time it is clicked with the following:

const LoggableComponent = withLogger(BaseComponent);

This can now be used anywhere BaseComponent was used previously, as it returned a component wrapping BaseComponent.

It should be noted that in this example, we passed all props from the render method to the wrapped component using the spread operator. This is typically advisable, as it allows the HoC to be used in place of the wrapped component.

A Few Variants

Another use of the HoC is to transform props of a wrapped component. For example, if we have numerous components with a title prop, and we want to create variants of components with capitalized tiles–or any other string transformation–we could create an HoC to handle the transformation:


const makeUpperCase = (WrappedComponent) => {
  return class UpperCaseComponent extends React.Component {
    render() {
      const props = Object.assign({}, this.props, {
        title: this.props.title.toUpperCase()
      });

      return <WrappedComponent { ...props } />
    }
  };
}

Here we mutate the props and pass the updated version to the wrapped component. Similar to before, we can create a higher-order component by just passing in a component to wrap.


const UpperCaseComponent = makeUpperCase(BaseComponent);

Now let’s check out an example that manages a bit of state:


const makeToggleable = (WrappedComponent, color) => {
  return class ToggleableComponent extends React.Component {
    constructor(props) {
      super(props);

      this.state = { toggled: false };
      this.toggleColor = this.toggleColor.bind(this);
    }

    toggleColor() {
      this.setState({ toggled: !this.state.toggled });
    }

    render() {
      const fontColor = this.state.toggled? color: 'black';
      return (
        <WrappedComponent { ...this.props }
          style={{color: fontColor}}
          onClick={this.toggleColor} />
      );
    }
  }
}
const ToggleableComponent = makeToggleable(BaseComponent, 'red');

This HoC is similar to the logger we created above, but its click handler alters the state, then updates the style based on the state and rerenders, passing the style to the wrapped component. An additional change is that the HoC takes in another component–the color to render when the state is toggled on. HoCs are not restricted to just taking a component; they may take any other arguments you like.

Wrapping Up

As a final note, I’d like to mention that since HoCs take components as input and return other components as output, HoCs may be passed other HoCs.

For example, we could combine two of the above examples to create a component that is toggle-able, and has an uppercase title with one line of code:


const UpperCaseToggleableComponent = makeUpperCase(makeToggleable(BaseComponent, 'red'));

Thanks for checking out out this post, and leave a comment if you have any interesting examples of higher-order components!

Conversation
  • Juraj says:

    You could try recompose.

  • Dave M says:

    Thanks for this! It would be helpful to see the other side of this coin: how the consumer code (the wrapped component definitions) import, call, and use this. It’s a little bit hard for me to infer that.

  • Amol says:

    Thank you so much for making it so clear. Also, I think it would be “It should be noted that in this example, we passed all props from ClickLogger to the wrapped component..”.

    • Tyler Hoffman Tyler Hoffman says:

      Glad it was helpful! And thanks for the feedback; I updated that line to reflect that line to be a bit more clear.

  • Comments are closed.