Skip to content

React HOCs (higher order components)

Created: 2018-04-18 16:07:03 -0700 Modified: 2018-08-10 14:24:39 -0700

If you want to specify parameters that never change to an HOC, you can do so like this:

HOC.js
const someHoc = params => ComposedComponent => {
return class extends Component {
someFunctionThatUsesParams() {
console.log(params.color);
}
render() {
return <ComposedComponent {...this.props}/>;
}
};
};
export default someHoc;
// Some other file
class WrappedComponent {
}
export default makeDraggable({color: 'green'})(WrappedComponent);

By doing things this way, you don’t have to use “bind” or anything.

Note that you definitely don’t want to do this with dynamic properties. For that, see this section.

Updating an HOC from the wrapped component

Section titled Updating an HOC from the wrapped component

Suppose you have a HOC to handle dragging DOM elements. You want to do something like this:

export default makeDraggable(InventoryItem)

However, the InventoryItem should only be considered draggable when some condition is true (e.g. the shopkeeper is selling it at the time).

In these cases, you may think that you should update the wrapper (the HOC) from the wrapped component, but it’s better to just have the HOC call a function in the child:

const makeDraggable = ComposedComponent => {
return class extends Component {
constructor(props) {
super(props);
/**
* A ref to the wrapped component.
* @type {Component}
*/
this.wrappedComponentRef = null;
}
onDragMove(event) {
if (!this.wrappedComponentRef.isDraggingAllowed()) {
return;
}
// drag code
}
render() {
return <ComposedComponent
ref={(node) => {this.wrappedComponentRef = node;}}
{...this.props}
/>;
}
};
};

If you’re going to do this, then it’s best not to just assume that the wrapped component has any functions used. The best way to do this is to make use of PropTypes, but the component itself has to implement the functions rather than having them be passed in as props. HiDeoo made a CodeSandbox for me here demonstrating how you would manifest these, but I added one extra feature to it (componentOptionallyImplements). Note that HiDeoo suggested that I follow the same paradigm of React for using “isRequired” (reference).

export const makeDraggable = params => ComposedComponent => {
ComposedComponent.propTypes = {
...ComposedComponent.propTypes,
isDraggingAllowed: componentImplements(ComposedComponent),
onStartDragItem: componentOptionallyImplements(ComposedComponent)
};
return class extends Component {/*methods here*/};
};

The definitions of componentImplements and componentOptionallyImplements are here:

export function componentImplements(component) {
return function(props, propName, componentName) {
if (!_.isFunction(component.prototype[propName])) {
return new Error(
`The \${componentName}\ component should implement the \${propName}\ method.`
);
}
};
}
export function componentOptionallyImplements(component) {
return function(props, propName, componentName) {
const existingProperty = component.prototype[propName];
if (!_.isNil(existingProperty) && !_.isFunction(existingProperty)) {
return new Error(
`The \${componentName}\ component has \${propName}\ defined, but it's not a function.`
);
}
};
}

The reasons why this is really helpful is because:

  • The wrapper can define what’s required as opposed to the wrapped component needing to add in something extra.
  • We get one consistent place to comment all of the props that the wrapper may expect (even if they’re optional).

Using a reference to a child of the wrapped component from the HOC

Section titled Using a reference to a child of the wrapped component from the HOC

Update:

[14:22] HiDeoo: If [you wrote the HOC] & [you’re using] React > 16.3 You can use the new ref system with React.createRef & specially React.forwardRef https://reactjs.org/docs/forwarding-refs.html

[14:24] HiDeoo: Also, React.createRef() is now the recommended API to use for refs but callback refs are still supported

In the HOC:

const makeDraggable = params => ComposedComponent => {
return class extends Component {
constructor(props) {
super(props);
/**
* This is set by the wrapped component. It's the particular element
* that is draggable. This has to be set in initializeRefs.
*/
this.refToDraggable = null;
}
/**
* This needs to be called by the wrapped component in order to set
* which element is draggable.
*/
initializeRefs({refToDraggable}) {
this.refToDraggable = refToDraggable;
}
render() {
return <ComposedComponent
initializeRefs={this.initializeRefs.bind(this)}
{...this.props}
/>;
}
};
};

From the wrapped component:

componentDidMount() {
this.props.initializeRefs({
refToDraggable: this.draggable,
});
}

HiDeoo: Adam13531 In a more advanced way, that’s what react-dnd is doing with a this.props.connectDropTarget(element) http://react-dnd.github.io/react-dnd/docs-drop-target-connector.html

Bypassing writing a component with the same props everywhere by using HoC (reference)

Section titled Bypassing writing a component with the same props everywhere by using HoC (reference)

I ran into a problem where there was a component that was used from many different containers. For example, I have a header component that has functions like “settings”, “store”, and “logout”, and I didn’t want to have to do this from every single page:

<Route path='landingPage'
component={LandingPage}
onLogout={logout}
/>
<Route path='news'
component={News}
onLogout={logout}
/>
<Route path='somethingElse'
component={SomethingElse}
onLogout={logout}
/>

HiDeo suggested that I do this:

/**
* PackOpener Higher-Order Component.
*/
export const withPacks = (ComposedComponent) => {
const composition = props => <ComposedComponent {...props} />;
composition.displayName = `withPacks(${ComposedComponent.displayName || ComposedComponent.name})`;
return connect(null, { openPackUI, openPack, sellPack })(composition); // note: connect comes from "react-redux"
};

Anywhere you can use a decorator to get this new actions as props

@withPackOpener
export default class Thingy extends Component {
....
}

Or the basic way by wrapping

withPackOpener(Thingy);

HiDeoo: I use that a lot for example for notifications, I have a Notifications HoC and in any component that need to raise a notificaiton, I use the @withNotifications decorator and can use this.props.addError(‘something went wrong’) in the component

I asked about the situation where I have exactly the same header in each page:

HiDeoo: Either have its own container or decorators + HoC but if you have 30+ header, forgetting the HoC wrapping in one may screw everything up ^^

HiDeoo: And instead of rendering <Header /> you render <HeaderContainer /> which then render <Header /> with all the proper props

Sometimes you want a higher-order component that provides some Redux state to the wrapped component:

import { connect } from 'react-redux';
import React from 'react'; // eslint-disable-line no-unused-vars
export default function withMediaQueryState(ComposedComponent) {
const composition = props => <ComposedComponent {...props} />;
composition.displayName = `withMediaQueryState(${ComposedComponent.displayName || ComposedComponent.name})`;
function mapStateToProps(state) {
return {
mediaQueryState: state.mediaQueryReducer,
};
}
return connect(mapStateToProps)(composition);
}

Note that if you only want to pass specific properties, you can just change how mapStateToProps looks:

function mapStateToProps(state) {
return {
singleProp: state.mediaQueryReducer.somePropertyHere,
};
}