Higher-order components in React

Original author: Tyler McGinnis
  • Transfer
We recently published a higher-order feature in javascript aimed at those who study javascript. The article, the translation of which we publish today, is intended for beginner React-developers. It is dedicated to higher order components (Higher-Order Components, HOC).



DRY principle and higher order components in React


You will not be able to advance far enough in the matter of studying programming and not encounter the almost cult principle of DRY (Don't Repeat Yourself, do not repeat). Sometimes his followers go too far, but, in most cases, it is worth striving to observe it. Here we will talk about the most popular React-development pattern, which allows you to enforce the DRY principle. We are talking about components of higher order. In order to understand the value of higher order components, let's first formulate and understand the problem for which they are intended.

Suppose you need to recreate a control panel similar to the Stripe panel. Many projects tend to evolve according to a scheme where everything is going great until the project is completed. When you think that the work is almost finished, you notice that the control panel has many different tooltips that should appear when you hover the mouse over certain elements.


Control Panel and tooltips

In order to implement this functionality, you can use several approaches. You decided to do this: determine whether the pointer is above a separate component, and then decide whether to show a hint for it or not. There are three components that need to be equipped with similar functionality. This Info, TrendChartandDailyChart.

Let's start with the component Info. Right now it is a simple SVG icon.

classInfoextendsReact.Component{
  render() {
    return (
      <svg
        className="Icon-svg Icon--hoverable-svg"
        height={this.props.height}
        viewBox="0 0 16 16" width="16">
          <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />
      </svg>
    )
  }
}

Now we need to make sure that this component can determine whether the mouse pointer is over it or not. You can use mouse onMouseOverand events for this onMouseOut. The function that is passed onMouseOverwill be called if the mouse pointer hits the component area, and the function passed onMouseOutwill be called when the pointer leaves the component. In order to organize all this in the manner that is accepted in React, we add a property to the component hoveringthat is stored in a state, which allows us to re-render the component, showing or hiding the tooltip, in case this property changes.

classInfoextendsReact.Component{
  state = { hovering: false }
  mouseOver = () => this.setState({ hovering: true })
  mouseOut = () => this.setState({ hovering: false })
  render() {
    return (
      <>
        {this.state.hovering === true
          ? <Tooltip id={this.props.id} />
          : null}
        <svg
          onMouseOver={this.mouseOver}
          onMouseOut={this.mouseOut}
          className="Icon-svg Icon--hoverable-svg"
          height={this.props.height}
          viewBox="0 0 16 16" width="16">
            <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />
        </svg>
      </>
    )
  }
}

It turned out well. Now we need to add the same functionality to two more components - TrendChartand DailyChart. The above mechanism for the component Infoworks fine, it’s not necessary to repair it, so let's recreate the same in other components using the same code. Rewrite component code TrendChart.

classTrendChartextendsReact.Component{
  state = { hovering: false }
  mouseOver = () => this.setState({ hovering: true })
  mouseOut = () => this.setState({ hovering: false })
  render() {
    return (
      <>
        {this.state.hovering === true
          ? <Tooltip id={this.props.id}/>
          : null}
        <Chart
          type='trend'
          onMouseOver={this.mouseOver}
          onMouseOut={this.mouseOut}
        />
      </>
    )
  }
}

You probably already understood what to do next. The same can be done with our last component - DailyChart.

classDailyChartextendsReact.Component{
  state = { hovering: false }
  mouseOver = () => this.setState({ hovering: true })
  mouseOut = () => this.setState({ hovering: false })
  render() {
    return (
      <>
        {this.state.hovering === true
          ? <Tooltip id={this.props.id}/>
          : null}
        <Chart
          type='daily'
          onMouseOver={this.mouseOver}
          onMouseOut={this.mouseOut}
        />
      </>
    )
  }
}

Now everything is ready. You may have written something similar on React. This, of course, is not the worst code in the world, but it does not follow DRY very well. As you can see, after analyzing the component code, we, in each of them, repeat the same logic.

The problem that we are facing now must become extremely clear. This is a duplicate code. To solve it, we want to get rid of the need to copy the same code in cases where what we have already implemented is necessary for the new component. How to solve it? Before we talk about this, let us dwell on several programming concepts that will greatly facilitate the understanding of the solution proposed here. We are talking about callbacks and higher order functions.

Higher order functions


Functions in JavaScript are first class objects. This means that they, like objects, arrays or strings, can be assigned to variables, passed to functions as arguments, or returned from other functions.

functionadd(x, y){
  return x + y
}
functionaddFive(x, addReference){
  return addReference(x, 5)
}
addFive(10, add) // 15

If you are not used to such behavior, then the above code may seem strange to you. Let's talk about what's going on here. Namely, we pass the function to the addfunction addFiveas an argument, rename it to, addReferenceand then call it.

When using similar constructions, a function passed to another as an argument is called a callback (callback function), and a function that receives another function as an argument is called a higher order function.

Naming entities in programming is important, so here is the same code used in which names are changed according to the concepts they represent.

functionadd(x,y) {
  return x + y
}
functionhigherOrderFunction(x, callback) {
  return callback(x, 5)
}
higherOrderFunction(10, add)

This pattern should seem familiar to you. The fact is that if you used, for example, methods of JavaScript arrays, worked with jQuery or with lodash, then you have already used both higher order functions and callbacks.

[1,2,3].map((i) => i + 5)
_.filter([1,2,3,4], (n) => n % 2 === 0 );
$('#btn').on('click', () =>
  console.log('Callbacks are everywhere')
)

Let's return to our example. What if, instead of creating a function addFive, we want to create more, and function addTen, and addTwenty, and the like. Considering the way the function is implemented addFive, we will have to copy its code and change it to create the above functions based on it.

functionadd(x, y){
  return x + y
}
functionaddFive(x, addReference){
  return addReference(x, 5)
}
functionaddTen(x, addReference){
  return addReference(x, 10)
}
functionaddTwenty(x, addReference){
  return addReference(x, 20)
}
addFive(10, add) // 15
addTen(10, add) // 20
addTwenty(10, add) // 30

It should be noted that the code we got is not so dreadful, but it is clearly visible that many fragments in it are repeated. Our goal is that we can create as many functions that add some numbers to the numbers allocated to them ( addFive, addTen, addTwenty, and so on) how much we need, while minimizing duplication of code. Maybe to achieve this goal we need to create a function makeAdder? This function can take a certain number and a link to the function add. Since the purpose of this function is to create a new function that adds the number passed to it to the given one, we can make the function makeAdderreturn a new function, in which a certain number is set (like the number 5 in makeFive) and which can accept numbers for addition with that number.

Let's look at an example of the implementation of the above mechanisms.

functionadd(x, y){
  return x + y
}
functionmakeAdder(x, addReference){
  returnfunction(y){
    return addReference(x, y)
  }
}
const addFive = makeAdder(5, add)
const addTen = makeAdder(10, add)
const addTwenty = makeAdder(20, add)
addFive(10) // 15
addTen(10) // 20
addTwenty(10) // 30

Now we can create as many addfunctions as needed, and at the same time minimize the amount of code duplication.

If it is interesting, the concept that there is a certain function that processes other functions so that they can be used with fewer parameters than before is called “partial use of the function”. This approach is used in functional programming. An example of its use is a method .bindused in JavaScript.

All this is good, but what has React and the above described problem of duplicating mouse event handling code when creating new components that need this feature? The fact is that just like a higher order functionmakeAdderhelps us to minimize duplication of code, what is called a “higher order component” will help us deal with the same problem in a React application. However, everything will look different here. Namely, instead of a work scheme in which a higher order function returns a new function that calls a callback, a higher order component can implement its own scheme. Namely, it is able to return a new component that renders the component playing the role of a callback. Perhaps we have already spoken a lot of things, so it's time to move on to examples.

Our higher order function


This feature has the following features:

  • It is a function.
  • She takes a callback as an argument.
  • It returns a new function.
  • The function that it returns can call the original callback that was passed to our higher order function.

functionhigherOrderFunction(callback) {
  returnfunction() {
    return callback()
  }
}

Our higher order component


This component can be characterized as follows:

  • It is a component.
  • It takes another component as an argument.
  • It returns a new component.
  • The component it returns may render the source component passed to a higher order component.

function higherOrderComponent (Component) {
  returnclassextendsReact.Component{
    render() {
      return <Component />
    }
  }
}

HOC implementation


Now that we have, in general terms, figured out exactly what actions a higher-order component performs, we will begin to make changes to our React-code. If you remember, the essence of the problem we are solving is that the code that implements the logic of handling mouse events has to be copied to all components that need this feature.

state = { hovering: false }
mouseOver = () =>this.setState({ hovering: true })
mouseOut = () =>this.setState({ hovering: false })

Given this, we need our higher-order component (let's call it withHover) to encapsulate the event handling code of the mouse, and then pass the property to the hoveringcomponents it renders. This will allow us to prevent duplication of the corresponding code by placing it in the component withHover.

Ultimately, this is what we want to achieve. Whenever we need a component that must have an idea of ​​its property hovering, we can transfer this component to a component of a higher order withHover. That is, we want to work with the components as shown below.

const InfoWithHover = withHover(Info)
const TrendChartWithHover = withHover(TrendChart)
const DailyChartWithHover = withHover(DailyChart)

Then, when what is returned withHoveris rendered, it will be the original component to which the property is transferred hovering.

function Info ({ hovering, height }) {
  return (
    <>
      {hovering === true
        ? <Tooltipid={this.props.id} />
        : null}
      <svg
        className="Icon-svg Icon--hoverable-svg"
        height={height}
        viewBox="0 0 16 16"width="16">
          <pathd="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />
      </svg>
    </>
  )
}

As a matter of fact, now we just have to implement the component withHover. From the above it can be understood that he must perform three actions:

  • Accept the Component argument.
  • Return a new component.
  • Render the Component argument, passing it the property hovering.

▍ Accept Component Argument


functionwithHover(Component) {
}

▍Return new component


function withHover (Component) {
  returnclassWithHoverextendsReact.Component{
  }
}

▍ Rendering a Component with hovering properties


Now we have the following question: how to get to the property hovering? In fact, we have already written the code for working with this property. We just need to add it to the new component, and then pass the property to it hoveringwhen rendering the component passed to the higher order component as an argument Component.

functionwithHover(Component) {
  returnclassWithHoverextendsReact.Component{
    state = { hovering: false }
    mouseOver = () =>this.setState({ hovering: true })
    mouseOut = () =>this.setState({ hovering: false })
    render() {
      return (
        <divonMouseOver={this.mouseOver}onMouseOut={this.mouseOut}>
          <Componenthovering={this.state.hovering} />
        </div>
      );
    }
  }
}

I prefer to talk about these things as follows (and this is what the React documentation says): a component converts properties to a user interface, and a higher-order component converts a component to another component. In our case, we transform the components Info, TrendChartand DailyChartinto new components, which, thanks to the property hovering, know whether the mouse pointer is over them.

Additional Notes


At this point, we have reviewed all the basic information about the components of higher order. However, there are still some important things to discuss.

If you look at our HOC withHover, you will notice that it has at least one weak spot. It implies that the receiving component of the hoveringproperty will not experience any problems with this property. In most cases, probably, this assumption is justified, but it may happen that this is unacceptable. For example, what if a component already has a property hovering? In this case, there will be a name conflict. Therefore, it is withHoverpossible to make a change to the component , which is to allow the user of this component to specify what name the property should be hoveringtransferred to the components. BecausewithHover - this is just a function, let's rewrite it so that it takes the second argument, which specifies the name of the property passed to the component.

functionwithHover(Component, propName = 'hovering') {
  returnclassWithHoverextendsReact.Component{
    state = { hovering: false }
    mouseOver = () =>this.setState({ hovering: true })
    mouseOut = () =>this.setState({ hovering: false })
    render() {
      const props = {
        [propName]: this.state.hovering
      }
      return (
        <divonMouseOver={this.mouseOver}onMouseOut={this.mouseOut}>
          <Component {...props} />
        </div>
      );
    }
  }
}

Now we set, thanks to the default parameter values ​​mechanism of ES6, the standard value of the second argument as hovering, but if the component user withHoverwants to change it, he can pass, in this second argument, the name he needs.

function withHover(Component, propName = 'hovering') {
  returnclassWithHoverextendsReact.Component{
    state = { hovering: false }
    mouseOver = () => this.setState({ hovering: true })
    mouseOut = () => this.setState({ hovering: false })
    render() {
      const props = {
        [propName]: this.state.hovering
      }
      return (
        <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>
          <Component {...props} />
        </div>
      );
    }
  }
}
function Info ({ showTooltip, height }) {
  return (
    <>
      {showTooltip === true
        ? <Tooltip id={this.props.id} />
        : null}
      <svg
        className="Icon-svg Icon--hoverable-svg"
        height={height}
        viewBox="0 0 16 16" width="16">
          <path d="M9 8a1 1 0 0 0-1-1H5.5a1 1 0 1 0 0 2H7v4a1 1 0 0 0 2 0zM4 0h8a4 4 0 0 1 4 4v8a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4zm4 5.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" />
      </svg>
    </>
  )
}
const InfoWithHover = withHover(Info, 'showTooltip')

The problem with the implementation of withHover


You may have noticed another problem in the implementation withHover. If we analyze our component Info, we can see that it, among other things, accepts a property height. The way we have everything now, means that it heightwill be installed in undefined. The reason for this is that the component withHoveris the component responsible for rendering what is passed to it as an argument Component. Now we are no properties but we have created hovering, the component Componentdo not share.

const InfoWithHover = withHover(Info)
...
return <InfoWithHover height="16px" />

The property is heightpassed to the component InfoWithHover. And what is this component? This is the component that we return from withHover.

functionwithHover(Component, propName = 'hovering') {
  returnclassWithHoverextendsReact.Component{
    state = { hovering: false }
    mouseOver = () =>this.setState({ hovering: true })
    mouseOut = () =>this.setState({ hovering: false })
    render() {
      console.log(this.props) // { height: "16px" }
      const props = {
        [propName]: this.state.hovering
      }
      return (
        <divonMouseOver={this.mouseOver}onMouseOut={this.mouseOut}>
          <Component {...props} />
        </div>
      );
    }
  }
}

Inside the component WithHoverthis.props.heightis equal 16px, but in the future we do not do anything with this property. We need to make this property be passed to the argument Componentthat we are rendering.

render() {
      const props = {
        [propName]: this.state.hovering,
        ...this.props,
      }
      return (
        <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>
          <Component {...props} />
        </div>
      );
}

About the problems of working with higher-order third-party components


We believe that you have already appreciated the advantages of using higher order components in reusing logic in various components without the need to copy the same code. Now let us ask ourselves whether the higher order components have flaws. This question can be answered positively, and we have already met with these shortcomings.

When using HOC, control inversion occurs . Imagine that we use a higher-order component that was not developed by us, like the HOC withRouterReact Router. In accordance with the documentation, withRouterpass the properties match, locationand historythe component wrapped by it when it is rendered.

classGameextendsReact.Component{
  render() {
    const { match, location, history } = this.props // From React Router
    ...
  }
}
export default withRouter(Game)

Note that we do not create an element Game(that is, - <Game />). We completely transfer our component React Router and trust this component not only rendering, but also transferring the correct properties to our component. Above, we already encountered this problem when we talked about a possible name conflict during property transfer hovering. In order to fix this, we decided to allow the HOC user to withHoveruse the second argument to set the name of the corresponding property. Using someone else's HOC, withRouterwe do not have this opportunity. If the component Gamealready uses properties match, locationor history, then we can say we are not lucky. Namely, we either have to change these names in our component, or refuse to use HOC withRouter.

Results


Speaking of HOC in React, you need to remember two important things. First, HOC is just a pattern. Higher-order components are not even specific to React, despite the fact that they are related to the architecture of the application. Secondly, in order to develop React applications, it is not necessary to know about higher order components. You may well be unfamiliar with them, but at the same time write excellent programs. However, as in any business, the more tools you have - the better can be the result of your work. And, if you write applications using React, you will do yourself a disservice by not adding a HOC to your arsenal.

Dear readers! Do you use higher order components in React?


Also popular now: