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.