Single-Prop HOCs – Better Composition in React
Blog

Single-Prop HOCs – Better Composition in React

Fulfilling the promise of UI components

When people think of UI components, they may think of front-end widgets like you get from Bootstrap. This only scratches the surface, though. An app should be able to assemble all of its functionality by taking these elements and combining them with functionality that provides persistence (database) and business logic (what your app actually does) – all while keeping all the parts modular. React and ES2015 modules are finally giving us the tools to fulfill this promise. We will explore a way to do this intuitively and cleanly.

A brief history of React composition

What I mean by composition here is the process of assembling all the pieces of your front end into a cohesive whole. I would argue that there are two kinds of composition in React:

  • Presentation: Placing components inside other components. This allows us to, say, place a checkbox component inside our todo item, which might itself be inside a todo list.
  • Functionality: Adding data or functionality to a component. This allows us to bring in data to display, or add methods to change data or change the app’s state. We would use this to access the list of current todos, or to create a new one.

Presentational composition is trivially easy with React and hasn’t changed since React’s early days. We simply import the component and place it where we want it.

Functional composition, on the other hand, has proven to be more challenging, and it took some work by the community to get where we are. We began with mixins. A mixin adds properties directly on the component by setting this.someProperty. It does allow real composition, but it does so by monkeying with the presentational component. What if the mixin sets a property that conflicts with something in your component? For this and other reasons, mixins have fallen out of favor.

We have since moved on to the concept of container components. This makes adding functionality to your component as simple as putting it inside a container component that does no presentation at all but provides data and methods in the form of props while also sometimes defining lifecycle behaviors.

Higher-Order Components (HOCs)

The process of making container components has evolved toward higher-order components. This post describes them in detail, but my favorite definition comes from the Recompose library:

a higher-order component (HOC) refers to a function that accepts a single React component and returns a new React component.

const EnhancedComponent = hoc(BaseComponent)

Note that by this definition, an HOC is not itself a component; it’s a function that returns a presentational component nested in a container component. The container should transparently pass through any props it receives. This means that you can compose HOCs functionally. For example, hoc1(hoc2(PresentationalComponent)) will create two container components around the PresentationalComponent. hoc1 contains hoc2, which contains our PresentationalComponent. If hoc1 sets a prop containing a list of todos, PresentationalComponent will have access to that list because hoc2 will pass it through untouched.

What’s in a name?

This is all very nice, but it’s not the easiest thing to understand. You can see it in the naming conventions people have tried around functional composition. When people started using containers, we saw a lot of components called something like todosListContainer which is both long and uninformative. We know this wraps the todosList, but what does it provide? And if we want to add more functionality, do we add it here or should we make another container (perhaps todosListContainerContainer!?). The name encourages you to place all of your additional functionality inside a single container, which is not a great idea.

More recently, amongst GraphQL users, we are seeing HOCs named something like withMyTodos() (adds a prop myTodos with query results) or withCreateTodo() (adds a createTodo() function to props). These are at least indicative of the functionality they provide, but with feels very clunky, like a throwback to Hungarian notation. Putting these together gives us withMyTodos(withCreateTodo(Todos)), which works but just doesn’t feel great. What if there were a pattern that both made everything compose nicely and also made naming easier?

A better way with compose()

Enter Recompose, which bills itself as “lodash for React.” Recompose proposes that we compose and manipulate our components and HOCs in a way that more closely resembles the functional programming approach that inspired them in the first place. This post will not cover Recompose in any depth, but we will use its compose() function to clean up our HOCs. compose allows you to do something like this:

import React from 'react';
import { compose } from 'recompose';

// HOC that adds props.myTodos with query result
import myTodos from '../api/todos/myTodos';

// HOC that adds props.createTodo() to
// create a new todo in the database
import createTodo from '../api/todos/createTodo';

// presentational component that displays todos
// and a form to create new todos.
// In a bigger app, we might prefer to import it:
//   import Todos from '../ui/Todos';
const Todos = ({ myTodos, createTodo }) => (
  <ul>
    {myTodos.map((todo) => <li>todo.goal</li>);}
  </ul>
  // ...etc
);

export default compose(myTodos, createTodo)(Todos);

In this example, compose(myTodos, createTodo)(Todos) is the same as myTodos(createTodo(Todos)), but it’s more readable. It clearly separates HOCs from components. We can also do away with the withSomething naming, instead naming the HOC after the prop we are adding to our presentational component. If we had imported our Todos component, we would have only needed four lines of code to define the component. We could then easily reuse Todos in another component, perhaps composing it with a teamTodos query for a team manager to use. If we needed to add an HOC to add another mutation (deleteTodo()) or to limit who can see it (isAdmin), we just import it, change the export to compose(myTodos, createTodo, deleteTodo, isAdmin)(Todos), and consume the props in the presentational component.

If the syntax of compose()() is confusing, here’s an explanation: compose() (called once/one set of parens) is just combining all the HOCs it receives into a single HOC. We then call that HOC on the presentational component to add all the functionality of the compose HOCs at once. We could have separated it into const container = compose(myTodos, createTodo) and then returned export default container(Todos). Also, see the next section.

What does this look like in real life?

To see HOC composition in situ, view it in the React devtools. Below we can see the output of our code. The HOC we are using is part of the Apollo GraphQL ecosystem, so the container component is named Apollo. Our Todos presentational component is nested in two Apollo containers, as expected. You can see that Todos receives both createTodo() and myTodos in props.

Going a little deeper, myTodos is provided by the outermost container (shown as <Apollo(Apollo(Todos))>). You can also see myTodos getting passed through the container for createTodos (you can see it listed after<Apollo(Todos)> in the screen capture).

You can imagine that adding a lot of props could lead to a lot of containers. I think that in practice this should not be the normal state of things. If it happens, ask yourself if you are putting too much functionality in one component. The performance impact of having more containers is probably not significant in most apps, but there is some discussion about it in this talk if you want to explore further.

Emerging pattern: Single-prop HOC

In our GraphQL work at OK GROW!, we use react-apollo (with some syntactic sugar from our own react-apollo-helpers package). It provides a single query or mutation as a prop on your component. You cannot combine multiple queries or mutations in one HOC. This limitation led incidentally to the pattern we just explored. Let’s call it a single-prop HOC (SP-HOC). Without changing our code, we can codify the pattern as follows:

  • add any data or method to a component as a single prop (never more than one)
  • need more functionality? Add another HOC and compose separately
  • compose with the recompose’s compose() function:
const completeComponent = compose(hoc1, hoc2, hoc3)(presentationalComponent)

This comes with the following advantages:

  • complete decoupling: isolate unrelated HOCs & import from anywhere (or keep in same file, up to you)
  • intuitive and readable
  • easy to add/remove HOCs
  • no more naming confusion - name HOCs after the prop they will provide (you can rename on import or with recompose library if necessary)
  • obvious how to combine unrelated HOCs (keep separate and compose at the end)

The single-prop aspect may not apply to every use case (perhaps a library wants to add multiple capabilities at once), but it’s surprisingly powerful.

SP-HOC is better than the decorator pattern

I would submit that SP-HOC is a better pattern than using the emerging decorator pattern to do the same thing. Using decorators would look something like this:

import myTodos from '../api/todos/myTodos';
import createTodo from '../api/todos/createTodo';

@myTodos
@createTodo
class Todos extends React.Component {
  render() {
    const { myTodos, createTodo } = this.props;
    return (
      <ul>
        {myTodos.map((todo) => <li>{todo.goal}</li>)}
      </ul>
    // ...etc
    );
  }
}

export default Todos;

This is an appealing pattern at first glance, but it has some drawbacks:

  • Decorators can only work on classes, not functions, which pulls us away from the preference for functional components whenever possible.
  • You can’t isolate your presentational component from the decorators for testing or reuse - the decorator is hard-coded in
  • The decorator standard is not set yet and could change. It is also less-widely implemented in standard Babel configs.
  • The only place you can add a decorator is at the component declaration. We can’t import and then decorate.
  • Having the composition happen at the end of the file feels better to me, though YMMV.

Creating HOCs

It turns out that providing HOCs is something we can automate. In fact, libraries such as react-komposer will allow you focus on the functionality you want rather than the intricacies of creating a good HOC, which may need to handle special situations like async. If you need to create an HOC from scratch, you may want to look at how Recompose does it.

You may also find that frameworks and libraries already provide what you need. As mentioned before react-apollo and react-apollo-helpers do this for you in Apollo GraphQL. If your framework provides a mixin or container API that’s not compliant with the HOC definition we need for compose(), you can easily wrap it to make a true HOC. For example, we use a lot of Meteor at OK GROW!, and it’s easy to define HOCs using existing tools.

compose(presentation, functionality)(React)

This pattern has greatly clarified my thinking and code. I’d love to know if it is helpful to you or if you have improvements or better patterns.

We'll share what we've learned, get tips and info in your inbox occasionally