A Deep Dive into the React HOC (2)

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>
);
}
}
const compY = HOC(compX);
function withWindowScroll(WrappedComponent) {
return class extends React.Component {
// ...
}
}
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}
/>
);
}
}
}
function PositionTracker({x, y}) {
return (
<div>
X: {x}, Y: {y}
</div>
);
}
const WindowScrollTracker = withWindowScroll(PositionTracker);
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}
/>
);
}
}
}
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)
);
const mapStateToProps = (state) => ({
userName: state.user.name,
userScore: state.user.score
});
const ConnectedComponent = connect(mapStateToProps)(MyComponent);
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
);
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} />
}
}
}
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
})
});

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
);
// Creates HOC applied with additional parameters
const enhance = connect(mapStateToProps, mapDispatchToProps);
// HOC receives only one parameter (component).
const EnhancedComponent = enhance(MyComponent);
const windowScrollOptions = {
wait: 30,
mapProps: ({y}) => ({
isScrollOnTop: y === 0
})
};
const enhance = withWindowScroll(windowScrollOptions);const EnhancedComponent = enhance(MyComponent);
import {compose} from 'redux';
import {connect} from 'react-redux';
// ...const enhance = compose(
withMousePosition(mousePositionOptions),
withWindowScroll(windowScrollOptions),
connect(mapStateProps, mapDispatchToProps)
);
const EnhancedComponent = enhance(MyComponent);
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 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.
}
}
}

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.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store