React helper-order components 💪🏼

Drew Goodwin
Apr 26, 2017 · 4 min read

Higher-order components are great. All of the benefits of mixins, without the headaches. I also find them helpful for quick and dirty debugging, troubleshooting, and performance analysis. Let’s explore a few examples of using helper-order components to make your job a little easier.

A perfect match with stateless functions 💍

Higher-order components are particularly helpful for quickly checking behavior of components that are stateless functions, because they can be easily added without changing the function itself (like changing to a class).

Say you have a CoolButton:

import React from “react”:
import { Button } from “somewhere”;
const CoolButton = props => (
<Button cool="true" {...props} />
);
export default CoolButton;

To peek at the props you change the function:

const CoolButton = props => {
console.log(props);

return <Button cool="true" {...props} />;
}

With a helper-order component, however, the footprint is smaller (the change is temporary so require is used inline):

export default require("hocs/logProps")(CoolButton);

For reference, here is a very basic implementation of logProps:

import React from "react";export default WrappedComponent => props => {
console.log(WrappedComponent.name, props)
return <WrappedComponent {...props} />;
};

Even better when already using higher-order components 👏🏾

If already using other higher-order components, especially with a utility like recompose, using logProps might require even bigger changes. Consider CoolButton in recompose:

import React from “react”:
import { Button } from “somewhere”;
import { compose, withProps, renderComponent } from "recompose";
export default compose(
withProps(props => {cool: true}),
renderComponent(Button)
)

Using the approach above, compose, withProps, and renderComponent would need to be unrolled and re-rolled when finished. A helper-order component requires just one additional line in the compose() pipeline:

export default compose(
withProps(props => {cool: true}),
require("hocs/logProps"),
renderComponent(Button)
)

Another example—counting renders ⏱

Say you’re making a game with a bunch of draggable Pieces (in this case, circles), which change color while being dragged:

import React from "react";
import { Circle } from "somewhere";
import draggable from "hocs/draggable";
import { compose } from "recompose";
const Piece = props =>
<Circle color={props.isDragging ? "red" : "blue"} />;
export default compose(
draggable
)(Piece);

The complexity of drag implementation is being hidden within draggable, but suffice it to say that components are wrapped in an element that is positioned to its current drag coordinates. It is also given an isDragging: boolean property indicating the drag status.

The Pieces are placed on a Board:

import React from "react";
import { View } from "react-native";
const Board = props => (
<View>
{props.pieces.map((piece, index) => (
<Piece key={index} />
))}
</View>
);
export default Board

With a few pieces everything is fine, but with many pieces drag performance becomes noticeably choppy. Only the coordinates of the dragged object are updated on each step of the drag movement, so ideally only the dragged Piece is re-rendered. Given the performance you surmise that the other Pieces are re-rendering as well. Profiling could be done in a few ways, but very quick and easy way is to log renders:

export default compose(
require("hocs/logRender"),
draggable)
)(Piece);

I did this in on a relatively complex example with many drag areas and pieces, and got the following output for just one small drag movement. Clearly there is a problem:

Console showing many renders for a simple drag movement.

Fortunately this is an easy problem to fix—we are able to use pure to let React know the Pieces do not need re-rendering unless the props change:

import { compose, pure } from "recompose";...export default compose(
require("hocs/logRender"),
pure,
draggable)
)(Piece);

Immediately the output shows a huge improvement for the same action, and performance is great again:

Console showing few renders after pure is used.

For reference, here is a very basic implementation of logRender:

import React from "react";export default WrappedComponent => props => {
console.log(WrappedComponent.name, "rendered");
return <WrappedComponent {...props} />;
};

And here is a slightly better version that keeps track of the count as shown in the example above:

import React, { Component } from "react";export default WrappedComponent => class Count extends Component {
constructor(props) {
super(props);
this.count = 0;
}
render() {
this.count = this.count + 1;
console.log(WrappedComponent.name, "rendered", this.count);
return <WrappedComponent count={this.count} {...this.props} />;
}
};

It should be noted that this only works for counting renders caused from the outside by prop updates. Stateful components updating their own state (or calling forceUpdate()) wouldn’t cause the helper to re-render and thus the count won’t be updated.

General tips

Whatever helper you create, here are a few tips to keep in mind:

  • Helper-order components can accept arguments, which you can use to make them more focused. For example, my version of logProps accepts a selector, which can be the name of a single prop, a list of props, or a function. If present, only props matching the selector are output.
  • Use WrappedComponent.name to make it easier to determine where the output is coming from.
  • Use JSON.stringify when logging objects to make it easier to see quickly without the need to expand manually.
  • It’s handy to adopt a standard syntax whereby adding helper/higher-order components requires minimal change:
import React from "react";const Widget = props => ...export default Widget

Now HOCs can be added just at the export line. Or to make it even easier:

import React from "react";
import { compose } from "recompose";
const Widget = props => ...export default compose()(Widget)

Drew Goodwin

Written by

The audience is us.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade