Codementor Events

Solving the problems of Higher Order Components without throwing the baby out with the bathwater

Published Oct 13, 2017Last updated Apr 10, 2018
Solving the problems of Higher Order Components without throwing the baby out with the bathwater

There have been some criticisms laid against Higher Order Components in recent articles — specifically “indirection” and “naming collisions”, that aren’t actually problems inherent with the HoC pattern, but are rather usage errors that can be easily avoided.

Explicitly defining prop names

Here is recompose’s withState HoC

withState("counter", "setCounter", 0)(({ counter, setCounter }) => (
  <div>
    {counter}
    <button onClick={() => setCounter(counter + 1)}>add 1</button>
  </div>
));

There’s no confusion about where the props counter and setCounter come from, and since you’re in control of what to name the props, collisions are also not a problem.

Let’s apply the same principle to the withMouse example taken from Michael Jackson’s Use a Render Prop!

import React from "react";
import ReactDOM from "react-dom";

// Allow propName to be passed in
const withMouse = propName => Component => {
  return class extends React.Component {
    state = { x: 0, y: 0 };

    handleMouseMove = event => {
      this.setState({
        x: event.clientX,
        y: event.clientY
      });
    };

    render() {
      return (
        <div style={{ height: "100%" }} onMouseMove={this.handleMouseMove}>
          <Component {...this.props} {...{ [propName]: this.state }} />
        </div>
      );
    }
  };
};

// Specify what name the propName should be returned as
const App = withMouse("mouse")(({ mouse }) => {
  const { x, y } = mouse;
  console.log(mouse);

  return (
    <div style={{ height: "100%" }}>
      <h1>
        The mouse position is ({x}, {y})
      </h1>
    </div>
  );
});

ReactDOM.render(<App />, document.getElementById("root"));

Since you are declaring what to name the prop you are about to receive right where it’s being used, there’s no issue of indirection or collisions.

What if your HoC needs to return multiple props?

Imagine that the withMouse HoC returned two props - position, which contains the x and y coordinates, and changeCounter, which keeps track of the number of times the position has changed.

Requiring each prop to be explicitly named would make the HoC more cumbersome to use with each additional prop that it provides. But without explicitly naming the props, you might end up with something like this:

compose(
  withKeyboard,
  withMouse,
  withHistory
)(({ position, changeCounter }) => {
  const { x, y } = position;
  return (
    <div style={{ height: "100%" }}>
      <h1>
        The mouse position is ({x}, {y}). 
        The position has updated {changeCounter} times.
      </h1>
    </div>
  );
}))

You could only guess that the position and changeCounter props came from withMouse and not the other HoCs, and you’ll have to assume that none of the other HoCs return props with the same names.

Namespacing Higher Order Components

compose(
  withKeyboard,
  namespace('mouse', withMouse),
  withHistory
)(({ mouse }) => {
  const { x, y } = mouse.position;
  return (
    <div style={{ height: "100%" }}>
      <h1>
        The mouse position is ({x}, {y}). 
        The position has updated {mouse.changeCounter} times.
      </h1>
    </div>
  );
}))

You could go even further and namespace all the HoCs (this works with any HoC that returns props)

compose(
  namespace('keyboard', withKeyboard),
  namespace('mouse', withMouse),
  namespace('history', withHistory)
)(({ mouse, keyboard, history }) => {
  const { x, y } = mouse.position;
  return (
    <div style={{ height: "100%" }}>
      <h1>
        The mouse position is ({x}, {y}). 
        The position has updated {mouse.changeCounter} times.
      </h1>
    </div>
  );
}))

Props are now traceable via namespace back to the HoC that provided them, and since namespaces are set when an HoC is being composed, collisions can be avoided as well.

The code for the namespace function is actually really simple

import { compose, withProps, mapProps } from 'recompose';

const namespace = (namespace, ...hocs) =>
  compose(
    withProps(props => ({ $parentProps: props })),
    ...hocs,
    mapProps(props => ({ [namespace]: props, ...props.$parentProps })),
  );

export default namespace;

Credits to Jeffrey Burt for the initial code solution and Ivan Starkov for making it more readable

Here’s a sandbox with it all together

Something Render Prop does better

Render Props does have the benefit in that you can sprinkle it into a render function. M_y_ friend Alex Wilmer has a great example demonstrating the utility of this that I hope will be turned into blog-form soon, but in the mean time here’s a simpler and more contrived example —

Let’s say you’re inside a large-ish block of JSX and there are some hardcoded and repeated variables (in this case, “Eastasia”) that you would like to DRY up.

const Content = () => (
  <div>
    {/* let's say there are a couple dozen lines of content above */}
    <div>
      ‘When I was arrested, Oceania was at war with Eastasia.’ ‘With Eastasia.
      Good. And Oceania has always been at war with Eastasia, has it not?’
      Winston drew in his breath. He opened his mouth to speak and then did not
      speak. He could not take his eyes away from the dial. ‘The truth, please,
      Winston. YOUR truth. Tell me what you think you remember.’ ‘I remember
      that until only a week before I was arrested, we were not at war with
      Eastasia at all. We were in alliance with them. The war was against
      Eurasia. That had lasted for four years. Before that ——’
    </div>
    {/* ... and a dozen lines of other content below */}
  </div>
);

You have a few options.

  1. Declare the variable up at the top of the function (which adherents to “declare variables close to where they are used” may not like)
  2. Create a separate component and pass the repeated values in (which is somewhat of a hassle)
  3. Create a component called <Assign/> that we can assign values to via props, then receive those values back in the render function.
const Assign = ({ render, ...props }) => render(props);

const Content = () => (
  <div>
    {/* let's say there are a couple dozen lines of content above */}
    <Assign
      enemy="Eastasia"
      ally="Eurasia"
      render={({ enemy, ally }) => (
        <div>
          ‘When I was arrested, Oceania was at war with {enemy}.’ ‘With {enemy}.
          Good. And Oceania has always been at war with {enemy}, has it not?’
          Winston drew in his breath. He opened his mouth to speak and then did
          not speak. He could not take his eyes away from the dial. ‘The truth,
          please, Winston. YOUR truth. Tell me what you think you remember.’ ‘I
          remember that until only a week before I was arrested, we were not at
          war with
          {enemy} at all. We were in alliance with them. The war was against
          {ally}. That had lasted for four years. Before that ——’
        </div>
      )}
    />
    {/* ... and a dozen lines of other content below */}
  </div>
);

Let’s go a little further and say that the values would come from async requests. You could make a <Resolve /> component that does this…

const Content = () => (
  <Resolve
    asyncValues={{
      enemy: () => Promise.resolve("Eastasia"),
      ally: () => Promise.resolve("Eurasia")
    }}
    render={({ enemy, ally }) => (
      <div>
        ‘When I was arrested, Oceania was at war with {enemy}.’ ‘With {enemy}.
        Good. And Oceania has always been at war with {enemy}, has it not?’
        Winston drew in his breath. He opened his mouth to speak and then did
        not speak. He could not take his eyes away from the dial. ‘The truth,
        please, Winston. YOUR truth. Tell me what you think you remember.’ ‘I
        remember that until only a week before I was arrested, we were not at
        war with
        {enemy} at all. We were in alliance with them. The war was against
        {ally}. That had lasted for four years. Before that ——’
      </div>
    )}
  />
);

Find the implementation of the <Resolve/> component at https://codesandbox.io/s/w8vp4k2pw

I’m not actually sure if using these components is a good idea (consult your nearest thought leader 😄), but it demonstrates where Render Props would allow a certain flexibility that, to my knowledge, HoC would not.

Summary

We seem to go through stages of

  1. “Huh, I didn’t realize you could do that”
  2. “This seems to be a great solution” (for certain use-cases)
  3. “This is the objectively superior solution!”
  4. “There are tradeoffs and pitfalls. Here is where this solution makes more sense, here is what you need to be careful about, and here is when it’s better to use something else.”

I think we’re at Stage 4 with Higher Order Components, there are traps that you could fall into, but also solutions to them as well. With Render Props, the articles I’ve seen so far seem to be still between Stage 2 and 3.

Everything is a tradeoff. I’m suspicious of a solution that’s presented as all benefit and no cost — that usually just means the costs haven’t been discovered yet 😄

Feel free to fill me in in the comments on what I’m missing, discuss on reddit, or tweet at me @CheapSteak


This article is sponsored by npmcharts.com 📈
Compare npm package download counts over time to spot trends and see which to use and which to avoid!

Here’s an interactive download chart for recompose

Discover and read more posts from Chang Wang
get started
post commentsBe the first to share your opinion
Show more replies