📜 ⬆️ ⬇️

Higher-order components in React

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. These are Info , TrendChart and DailyChart .

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

 class Info extends React.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 the mouse events onMouseOver and onMouseOut . The function that is passed onMouseOver will be called if the mouse pointer hits the component area, and the function passed to onMouseOut will be called when the pointer leaves the component. In order to organize all this in the manner that is accepted in React, we add the hovering property to the component, which is stored in the state, which allows us to re-render the component, showing or hiding the tooltip, if this property changes.

 class Info extends React.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 - TrendChart and DailyChart . The above mechanism for the Info component works fine, it’s not necessary to repair it, so let's recreate the same thing in other components using the same code. TrendChart component TrendChart .

 class TrendChart extends React.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 .

 class DailyChart extends React.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.

 function add (x, y) { return x + y } function addFive (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 add function to the addFive function as an argument, rename it to addReference and 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.

 function add (x,y) { return x + y } function higherOrderFunction (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 only the addFive function, we want to create the addTen function, and addTwenty , and other similar functions. Considering how the addFive function is addFive , we will have to copy its code and change it to create the above-mentioned functions based on it.

 function add (x, y) { return x + y } function addFive (x, addReference) { return addReference(x, 5) } function addTen (x, addReference) { return addReference(x, 10) } function addTwenty (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 could create as many functions that add certain numbers to the numbers passed to them ( addFive , addTen , addTwenty , and so on) as we need, while minimizing code duplication. Maybe to achieve this goal we need to create a function makeAdder ? This function can take a certain number and a link to the add function. Since the purpose of this function is to create a new function that adds the number passed to it to the specified one, we can make the function makeAdder return a new function, in which a certain number is specified (like the number 5 in makeFive ), and which could accept the numbers for addition with this number.

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

 function add (x, y) { return x + y } function makeAdder (x, addReference) { return function (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 add functions as needed, and at the same time minimize the amount of duplication of code.

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 the .bind method used 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 makeAdder higher-order makeAdder helps us minimize code duplication, what’s 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:


 function higherOrderFunction (callback) { return function () {   return callback() } } 

Our higher order component


This component can be characterized as follows:


 function higherOrderComponent (Component) { return class extends React.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 hovering property to the components it renders. This will allow us to prevent duplication of the corresponding code by placing it in the withHover component.

Ultimately, this is what we want to achieve. Whenever we need a component that needs to be aware of its hovering property, 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 withHover is rendered, it will be the source component to which the hovering property is passed.

 function Info ({ hovering, height }) { return (   <>     {hovering === 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>   </> ) } 

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


â–Ť Accept Component Argument


 function withHover (Component) { } 

â–ŤReturn new component


 function withHover (Component) { return class WithHover extends React.Component { } } 

â–Ť Rendering a Component with hovering properties


Now we have the following question: how to get to the hovering property? 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 hovering property to it when rendering the component passed to the higher order component as the Component argument.

 function withHover(Component) { return class WithHover extends React.Component {   state = { hovering: false }   mouseOver = () => this.setState({ hovering: true })   mouseOut = () => this.setState({ hovering: false })   render() {     return (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component hovering={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 convert the Info , TrendChart and DailyChart components into new components that, thanks to the hovering property, 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 point. It implies that the receiving component of the hovering property 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 hovering property? In this case, there will be a name conflict. Therefore, a withHover can be made to the withHover component, which is to allow the user of this component to indicate what name the hovering property should pass to the components. Since withHover is just a function, let's rewrite it so that it withHover second argument that specifies the name of the property passed to the component.

 function withHover(Component, propName = 'hovering') { return class WithHover extends React.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>     );   } } } 

Now we set the default value of the second argument as hovering , thanks to the default parameter setting mechanism of ES6, but if the user of the withHover component wants to change this, he can pass, in this second argument, the name he needs.

 function withHover(Component, propName = 'hovering') { return class WithHover extends React.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 with the withHover implementation. If we analyze our Info component, we can see that it, among other things, accepts the height property. The way everything is arranged now means that the height will be set to undefined . The reason for this is that the withHover component is the component responsible for rendering what is passed to it as the Component argument. Now we have no properties, except for the hovering we created, we do not pass the Component component.

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

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

 function withHover(Component, propName = 'hovering') { return class WithHover extends React.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 (       <div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>         <Component {...props} />       </div>     );   } } } 

Inside the WithHover component this.props.height is 16px , but we do not do anything with this property in the future. We need to make this property be passed to the Component argument, which 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 are using a higher-order component that was not developed by us, like the HOC withRouter React Router. In accordance with the documentation, withRouter will pass the match , location and history properties to the wrapped component when it is rendered.

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

Please note that we do not create a Game element (i.e., <Game /> ). We completely transfer our component React Router and trust this component not only rendering, but also transferring the correct properties to our component. We have already encountered this problem above when we talked about possible name conflicts when passing hovering properties. In order to fix this, we decided to allow the HOC withHover use the second argument to set the name of the corresponding property. Using someone else's HOC withRouter we do not have this opportunity. If the match , location or history properties are already used in the Game component, then we can say that 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?

Source: https://habr.com/ru/post/428572/


All Articles