Why Higher Order Components Make Sense

Higher Order Components compose!

TL;DR:

Higher Order Components compose. Once you need to combine multiple enhancing components, Higher Order Components standout. If you only need a single enhancing functionality, all known patterns (view as prop, children functions etc.) will work. Choosing which is a matter of preference and sometimes Higher Order Components are not the best solution for a given problem anyway.

Basics

This is just a very short write-up, to show why Higher Order Components make sense and where they really shine. The concept of Higher Order Components became popular when support for mixins was dropped as ES6 classes were introduced in React. Up to this point, mixins where a way to enhance a React component with additional functionality.

This is also exactly what Higher Order Components do. Enhance a component with functionality. So a Higher Order Component is a function that accepts a component and returns a component. Similar to higher order functions that accept a function and return a function.

Let’s take the following Higher Order Component as an example. It does one thing: transform props and call the wrapped component with the transformed props. Neglect the fact, that the function is not optimized, this implementation is strictly for explanation purposes. So we have a function that accepts a transform function (fn), then wraps a Component and finally transforms the props and calls the Component with these props.

const mapProps = fn => Component => props => 
React.createFactory(Component)(fn(props))

Next we define the mapping function and then enhance a stateless functional component with this transformation capability.

const SomeStatelessFunction = ({ id, updateState,...props }) => {
return <div>
<div>Id: {id}</div>
</div>
}

const Enhanced =
mapProps(({value, ...props}) => ({ ...props, ...{ id: value }}))

const EnhancedStatelessFunction = Enhanced(SomeStatelessFunction)

const App = () => {
return (
<div>
<EnhancedStatelessFunction
value=
{15}
/>
</div>
)
}

Now you might be thinking, why? You could achieve the same outcome with a simple Component that receives the mapping function and a component as prop and call the component after applying the passed in function. Let’s see how this can be achieved.

const MapProps = ({ fn, view: Component, ...props }) => {
const transformedProps = fn(props)
return (
<Component {...transformedProps} />
)
}
const App = () => {
return (
<div>
<MapProps
value='5'
fn=
{({value, ...props}) => ({ ...props, ...{ id: value }})}
view={SomeStatelessFunction}
/>
</div>
)
}

So yes, the outcome is the same. You could even take it a step further and pass in a child function via JSX.

const SomeStatelessFunction = ({ id,...props }) => (
<div>
<div>Id: {id}</div>
</div>
)

const App = () => {
return (
<div>
<MapProps
value='5'
fn=
{({value, ...props}) => ({ ...props, ...{ id: value }})}
>
{props => <SomeStatelessFunction {...props} />}
</MapProps>
</div>
)
}

All these approaches work and up to the individual developer to decide which one fits their style best.

Just by looking at these examples, it’s neither obvious nor clear what Higher Order Components can do that other patterns can’t. There are many ways to achieve the same, as seen with the above examples.


Composition

From here on out, we will think about composition. We would like to enhance our components with more than just a single functionality. So you might have written a second component, that solely takes care of managing local state for any given stateless function. You might have written it following the same pattern as the above examples, either as a Higher Order Component, via passing in props or via child functions.

Now let’s see how we can combine multiple functionalities with our component. To start things off, here is our implementation using compose, creating an enhance function and passing our component to enhance, to retrieve a component with state keeping and prop transformation capabilities.

const WithState = Component => 
class extends React.Component {
constructor(props) {
super(props)
this.state = props.initialModel
}

updateState = (updateState) => {
this.setState(updateState)
}

render() {
return React.createElement(Component, {
...this.props,
...this.state,
...{ updateState: this.updateState }
})
}
}

By using compose, we combine MapProps and WithState and create a new enhancer function. Forget the simplified compose function, in a real world scenario you would be using Ramda or Recompose or other utility libraries.

const mapProps = fn => Component => props =>
React.createFactory(Component)(fn(props))

const compose = (f, g) => x => f(g(x))

const Enhance = compose(
mapProps(({
initialModel: { value, ...initialValues },
...props
}) => ({
...props,
...{ ...initialValues, ...{ id: value } },
})),
WithState,
);

const SomeStatelessFunction = ({ id, updateState,...props }) => {
return <div>
<div>Id: {id}</div>
<button
onClick=
{() => updateState({ id: id + 1 })}
>
Update
</button>
</div>
}

const Enhanced = Enhance(SomeStatelessFunction)

const App = () => {
return (
<div>
<Enhanced initialModel={{ value: 5 }} />
</div>
)
}

If you have been using Recompose, then this will be nothing new or special. Recompose is optimized for this approach, offering a large number of utility functions, ranging from state keeping to adding lifecycle methods.

Next, let’s try to tackle the problem with the other aforementioned approaches. Let’s begin with passing in the component as a view prop.

class StateComponent extends React.Component {
constructor(props) {
super(props)
this.state = props.initialModel
}

updateState = (updateState) => {
this.setState(updateState)
}

render() {
return React.createElement(this.props.view, {
...this.props,
...this.state,
...{ updateState: this.updateState }
})
}
}

And then use it directly when rendering.

const App = () => {
return (
<div>
<StateComponent
view=
{SomeStatelessFunction}
initialModel={{id: 0}}
/>
</div>
)
}

This works as expected. But how do we combine MapProps and StateComponent?

Here’s a first naive try.

// will not work!
const App = () => {
return (
<div>
<MapProps
view=
{StateComponent}
value='5'
fn=
{({value, ...props}) => ({ ...props, ...{ id: value }})}
initialModel={{id: 0}}
/>
</div>
)
}

We seem to hit a limitation here. If our components expect a Component via view prop, then how can we pass in the stateless function we want wrapped with the extended functionalities? This doesn’t seem to work. Also, it would mean that we would have to rewrite our current enhance components, to make them workable with this given situation. Not ideal.

But we still have the child functions pattern. So, again how do we combine MapProps and StateComponent?

It appears that composition will work following this pattern.

const App = () => {
return (
<div>
<MapProps
fn=
{({value, ...props}) => ({ ...props, ...{ id: value }})}
initialModel={{id: 3}}
>
{props =>
<StateComponent {...props}>
{props => <SomeStatelessFunction {...props} />}
</StateComponent>
}
</MapProps>
</div>
)
}

While this works, it also means every component has to be wrapped inside a function expecting props. This is a matter of preference. But one must admit that the higher order example is very clear to understand.

To wrap things up:

If you need to compose a large number of enhancing functions, then using Higher Order Components is a very elegant way to do so!

const BaseComponent = props => {...}
const enhance = compose(
withState(/*...args*/),
mapProps(/*...args*/),
pure
)
const EnhancedComponent = enhance(BaseComponent)

(* above example is from the Recompose README)

Also important to note: sometimes you don’t need a Higher Order Composition or composition. Verify that Higher Order Components are the right solution to an existing problem first.

Another tip: A Higher Order Component should focus on a single functionality. For example adding lifecycle hooks and if you need debugging capabilities, move this functionality to a new Component and add via composition.

If you already use libraries like Ramda and are used to composition, than this style fits very well. It’s important to highlight the fact that this all comes down to preference.

If there are better ways to solve these problems, please leave a comment here or on Twitter.