A Deep Dive into the React HOC (2)

TOAST UI
8 min readJul 30, 2018

--

In the previous chapter, we learned what HOF (Higher Order Function) and HOC (Higher Order Component) are about and how they could be applied. Still, you may not fully understand the actual application of HOC for a project.

In this document, let’s create more practical examples of HOC and delve into problems that may occur during the process as well as ways to solve them.

Basic Exercise: Trace Window Scrolls

Let’s start with a simple exercise. I want to create a component that shows the location of windows scroll every time it changes. To this end, an event handler of scroll of window must be registered and unregistered, each when the component is mounted and unmounted.

class WindowScrollTracker extends React.Component {
state = {
x: 0,
y: 0
}
scrollHandler = () => {
this.setState({
x: window.pageXOffset,
y: window.pageYOffset
});
};
componentDidMount() {
window.addEventListener('scroll', this.scrollHandler);
}

componentWillUnmount() {
window.addEventListener('scroll', this.scrollHandler);
}
render() {
return (
<div>
X: {this.state.x},
Y: {this.state.y}
</div>
);
}
}

It needs some effort, but the implementation is simple requiring not such long codes. However, what if there are many components that need to react whenever a window scroll is relocated? Then, the logic of a same format must be duplicated at each component. This is so-called Cross-Cutting Concerns, and with HOC, code duplication can be efficiently removed.

Now, let’s make use of the above component code to create HOC. To start with, we need to look back on how HOC is defined.

const compY = HOC(compX);

HOC is a function that receives a component as parameter and returns a new component. In other words, our first job is to create a function.

function withWindowScroll(WrappedComponent) {
return class extends React.Component {
// ...
}
}

The newly-returned component class is the same as WindowScrollTracker, created at the beginning, except the render method. In the render method, render WrappedComponent, which is the received parameter of the withWindowScroll function, while sending down x, y information managed by state as props.

Note that as WrappedComponent may have its own props, other than x,y, all props should be delivered.

function withWindowScroll(WrappedComponent) {
return class extends React.Component {

// ... Other codes are same as WindowSizeTracker.

render() {
return (
<WrappedComponent
{...this.props}
x={this.state.x}
y={this.state.y}
/>
);
}
}
}

Then, let’s redefine WindowSizeTracker, created at the beginning, by using the withWindowScroll HOC function. Remove all, except the render method from the existing logic, and call HOC to define a new component. Now, in the render method, props received from withWindowScroll, instead of this.state are available.

function PositionTracker({x, y}) {
return (
<div>
X: {x}, Y: {y}
</div>
);
}
const WindowScrollTracker = withWindowScroll(PositionTracker);

The HOC can be used by any component requiring the application of changed window scroll locations. For example, for a component that shows “Top” only when the scroll is located on top, the implementation can be simple as below:

function TopStatus({y}) {
return (
<div>
{y === 0 && "It's on Top!!"}
</div>
);
}
const WindowScrollTopStatus = withWindowScroll(TopStatus);

Customizing HOC 1: Add Parameters

Let’s develop further on the exercise. What if a component using withWindowScroll wants to throttle scroll events? As each component may want different wait value, additional parameters must be received for processing. Then, let’s modify the code to receive the second parameter as an object, so as to receive custom value as wanted.

import {throttle} from 'lodash-es';function withWindowScroll(WrappedComponent, {wait = 0} = {}) {
return class extends React.Component {
// Other codes are same // For code simplification, let’s use the throttle function
// even when the wait value is 0.
scrollHandler = throttle(() => {
this.setState({
x: window.pageXOffset,
y: window.pageYOffset
});
}, wait);
render() {
return (
<WrappedComponent
{...this.props}
x={this.state.x}
y={this.state.y}
/>
);
}
}
}

As a result, each component now can use their wanted throttle values as below:

const WindowScrollTracker = withWindowScroll(
PositionTracker,
{wait: 30}
);
const WindowScrollTopStatus = withWindowScroll(
TopStatus,
{wait: 100}
);

Customizing HOC 2: Props Mapper

Components that are returned through the withWindowScroll function are injected with x, y as props. However, what about using a nested HOC that injects props in the same name? For example, let’s assume the use of the HOC named withMousePosition, which injects the mouse location in the name of x, y, as below.

const SinglePositionTracker = withMousePosition(
withWindowScroll(PositionTracker)
);

In this case, x, y injected through withWindowScroll shall be overrun by another x, y injected through withMousePosition. One of the commonly-mentioned disadvantages of HOC is that props name of nested HOC may be conflicted, and it can be simply resolved by providing a mapper function.

To better understand it, think of mapStateToProps which is used in the connect function of the familiar React-Redux Library. One of the functions of the connect function is to inject state stored at a store to a component. Here, by making use of the mapStateToProps function, you can select only the state that a component wants and also specify the name of props.

const mapStateToProps = (state) => ({
userName: state.user.name,
userScore: state.user.score
});
const ConnectedComponent = connect(mapStateToProps)(MyComponent);

As such, MyComponent gets props named userName and userScore only, instead of the entire state. Likewise, when calling HOC functions, by delivering the mapper function that returns the props you want injection for as objects, conflicts of name can be resolved.

function PositionTracker({scrollX, scrollY, mouseX, mouseY}) {
return (
<div>
ScrollX: {scrollX},
ScrollY: {scrollY},
mouseX: {mouseX},
mouseY: {mouseY}
</div>
);
}
const windowScrollOptions = {
wait: 30,
mapProps: ({x, y}) => ({
scrollX: x,
scrollY: y
})
};
const mousePositionOptions = {
mapProps: ({x, y}) => ({
mouseX: x,
mouseY: y
})
};
const EnhancedPositionTracker = withMousePotision(
withWindowScroll(PositionTracker, windowScrollOptions),
mousePositionOptions
);

Now, let’s modify the withWindowScroll function more, so as to support the mapProps function.

import {throttle, identity} from 'lodash-es';function withWindowScroll(WrappedComponent, {wait = 0, mapProps = identity} = {}) {  // Other codes are same  return class extends React.Component {
render() {
const {x, y} = this.state;
const passingProps = mapProps({x, y});
return <WrappedComponent {...this.props} {...passingProps} />
}
}
}

By using mapProps, completely different data can be delivered to props through calculation. For instance, in the case of TopStatus implemented earlier, all is required is to know whether the scroll-Y is 0 or not, and mapProps can help to implement more efficiently as below:

class TopStatus extends React.PureComponent {
render() {
return (
<div>
{this.props.isScrollOnTop && "It's on Top!!"}
</div>
);
}
}
const ScrollTopStatus = withWindowScroll(TopStatus, {
wait: 30,
mapProps: ({y}) => ({
isScrollOnTop: y === 0
})
});

In the above, you can find TopStatus is PureComponent. Indeed, all we need is to check whether the scroll-Y of TopStatus is 0 or not, but in the existing implementation, new props are received every time scroll value changes, creating unnecessary rendering constantly. However, the changed code receives props only when the isScrollOnTop value changes, so as to prevent unnecessary rendering by using PureComponent.

Nesting HOC — Compose

Let’s look through again how withMousePosition and withWindowScroll are nested among codes used in the above:

const EnhancedPositionTracker = withMousePotision(
withWindowScroll(PositionTracker, windowScrollOptions),
mousePositionOptions
);

As function calls and each parameter values are nested, it’s not easy to get it clearly. If HOCs are nested even more, you’ll find harder time to recognize codes.

To resolve this issue, React recommends a convention for HOC. Convention is similar to the connect function of React-Redux. Like below, the connect function refers to a function that returns HOC, not HOC itself.

// Creates HOC applied with additional parameters
const enhance = connect(mapStateToProps, mapDispatchToProps);
// HOC receives only one parameter (component).
const EnhancedComponent = enhance(MyComponent);

In other words, allow the HOC function to receive only one parameter (component) at all times, by providing another function to this end. Changing API of withWindowScroll to fit for this format should result in the following:

const windowScrollOptions = {
wait: 30,
mapProps: ({y}) => ({
isScrollOnTop: y === 0
})
};
const enhance = withWindowScroll(windowScrollOptions);const EnhancedComponent = enhance(MyComponent);

When all HOCs receive only one parameter, it will end up in a format like Component => Component, helping to process nested HOCs more elegantly by using libraries, like compose of Redux or flow of Lodash. For instance, let’s assume there is a total of three HOCs to nest, including the connect function of React-Redux in our previous exercise. In this case, using the compose function helps to write the code as simply as follows:

import {compose} from 'redux';
import {connect} from 'react-redux';
// ...const enhance = compose(
withMousePosition(mousePositionOptions),
withWindowScroll(windowScrollOptions),
connect(mapStateProps, mapDispatchToProps)
);
const EnhancedComponent = enhance(MyComponent);

Supporting this with the withWindowScroll function is not difficult: wrap the logic which returned existing components with the function once again.

function withWindowScroll({wait = 0, mapProps = identity} = {}) {
return function(WrappedComponent) {
return class extends React.Component {
// The class code is same as the existing one.
}
}
}

Debugging- Display Name

I have one last thing to point out: writing a code like in the above example may result in a component returned by actual HOC without a name, which is cited as one of the disadvantages of HOC. This, through React developer tool, will look like this:

As such, names like _class3, _class2 do not show precise information on components for debugging. That’s why React is supposed to specify a displayName for components returned by HOC, according to its conventions.

The above figure shows that a component returned by the connect function of React-Redux is named with Connect(_class3).The convention stipulates the HOC name start with an upper case letter followed by displayName of the component within parenthesis. To apply it to the withWindowScroll function, write as follows:

// As displayName of a component may be specified or not
// this helper function is required.
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName
|| WrappedComponent.name
|| 'Component';
}
function withWindowScroll({wait = 0, mapProps = identity} = {}) {
return function(WrappedComponent) {
return class extends React.Component {
// The static field grammar at the moment is on Stage 3.
// Where this is unavailable, static members of the class
// can be directly specified from outside.
static displayName =
`WithWindowScroll(${getDisplayName(WrappedComponent)})`;
// Other codes are same as before.
}
}
}

As a consequence, codes are more complicated, but following conventions will surely help for easy debugging in the future. With such modification, the developer tool will display the following:

Conclusion

In Chapter 2, we touched upon simple HOCs and some methods and conventions, with more functions added. If you’ve learned well enough, you’ll find no problems in making use of HOC in actual projects.

I mainly focused on the usage and good points of HOC but it has disadvantages as well. For example, to test nested HOC with shallow rendering or use static members of WrappedComponent, additional codes are required, and it is not easy to receive necessary information from WrappedComponent and process it for a rendering.

This has been translated into more interest in Render Props, and the official website of React, starting this year, has also added Advanced Guides of Render Props. Its popularity has grown more, even as the Context API on its latest 16.3.0 version adopts Render Props. If you’re interested in more details, I recommend you read Use a Render Prop! , which is a famous article on Render Props.

Nevertheless, I don’t personally consider Render Props as a better concept than HOC, because each of them contains pros and cons (and let me elaborate on them, in my future articles). If properly used, HOC can be made into a more flexible structure through the composition of components, while duplicated codes of React are completely removed.

Originally posted at Toast Meetup written by DongWoo Kim.

--

--

TOAST UI
TOAST UI

Responses (2)