Reusable State with Higher Order Components

By Dave Ceddia Comment

Higher-order components are a great way to do some next-level refactoring to your React code. If you have repeated state or lifecycle methods that you’d like to deduplicate, HOCs can help you extract that code into reusable functions. But they can be a little gnarly to wrap your head around at first. Functions returning functions returning components?!

But first: what exactly is a “higher-order component” (or HOC)?

Higher Order

The name comes from “higher-order function,” which is a fancy name for a function that accepts another function as an argument, and/or returns a function. You probably already use them without thinking about it. Array.forEach is a higher-order function, as is Array.map, setTimeout, and many more.

Those examples are all of the “accepts a function as an argument” variety, and for me, I think those are the easiest to understand at first glance. The real mind-bending happens when functions start returning functions.

// Ok :)
setTimeout(function() {
  // do a thing after 500ms
}, 500);

// Sure...
[1, 2, 3].map(function(i) {
  // multiply each element by 2
  return i * 2;
});

// Wait what?
function middleware(store) {
  return function(next) {
    return function(action) {
      // do the thing
    }
  }
}
// a.k.a.   const middleware = store => next => action => { }

So what’s a higher-order component, again? It’s a component that takes another component as an argument, and returns a new one that’s modified in some way. Now, since it takes another component as an argument, that means it must be a function, right? Right.

A typical HOC will follow this pattern:

// It's a function...
function myHOC() {
  // Which returns a function that takes a component...
  return function(WrappedComponent) {
    // It creates a new wrapper component...
    class TheHOC extends React.Component {
      render() {
        // And it renders the component it was given
        return <WrappedComponent {...this.props} />;
      }
    }

    // Remember: it takes a component and returns a new component
    // Gotta return it here.
    return TheHOC;
  }
}

Clear as mud? Yeah probably. Let’s look at a real example.

Extracting Shared State

Here are two components that both need to load the same “book” data, so they have identical componentDidMount functions but their render functions differ slightly:

BookDetails.js
import React, { Component } from 'react';
import * as API from '../api';  // let's just pretend this exists

class BookDetails extends Component {
  constructor(props) {
    super(props);
    this.state = {
      book: null
    };
  }

  componentDidMount() {
    API.getBook(this.props.bookId).then(book => {
      this.setState({ book });
    })
  }

  render() {
    const { book } = this.state;

    if(!book) {
      return <div>Loading...</div>;
    }

    return (
      <div>
        <img src={book.coverImg}/>
        <div>{book.author}</div>
        <div>{book.title}</div>
      </div>
    );
  }
}

export default BookDetails;
BookSummary.js
import React, { Component } from 'react';
import * as API from '../api';  // let's just pretend this exists

class BookSummary extends Component {
  constructor(props) {
    super(props);
    this.state = {
      book: null
    };
  }

  componentDidMount() {
    API.getBook(this.props.bookId).then(book => {
      this.setState({ book });
    })
  }

  render() {
    const { book } = this.state;

    if(!book) {
      return <div>Loading...</div>;
    }

    return (
      <div>
        <div>{book.summary}</div>
      </div>
    );
  }
}

export default BookSummary;

1. Identify Duplicate Code

Ignoring for a moment that this is a very simple example, there is some duplicated code here. The constructor and componentDidMount methods do the same thing in each component.

There’s also the “Loading…” text that appears while the book is being fetched.

Let’s see how we can extract these methods with a higher-order component.

2. Move Duplicate Code into HOC

We’ll take the HOC skeleton code from above and fill in the blanks:

BookLoader.js
import * as API from 'api'; // let's just pretend this exists

// It's a function...
function loadBook() {
  // Which returns a function that takes a component...
  return function(WrappedComponent) {
    // It creates a new wrapper component...
    class BookLoader extends React.Component {
      // Here's the duplicated code from above:
      constructor(props) {
        super(props);
        this.state = {
          book: null
        };
      }

      componentDidMount() {
        API.getBook(this.props.bookId).then(book => {
          this.setState({ book });
        })
      }

      render() {
        const { book } = this.state;

        if(!book) {
          return <div>Loading...</div>;
        }

        // Notice how "book" is passed as a prop now 
        return (
          <WrappedComponent
            {...this.props}
            book={book} />
        );
      }
    }

    // Remember: it takes a component and returns a new component
    // Gotta return it here.
    return BookLoader;
  }
}

export default loadBook;

Now the book state is handled by the BookLoader HOC, and it’s passed down to the wrapped component as a prop. It also transparently handles the “Loading” state too. What we’ve effectively done is “pull up” the state into the HOC. All that’s left is to update the old components to use it.

3. Wrap the Component, and Replace State With Props

Here are the new BookDetails and BookSummary components, refactored to use the new BookLoader HOC:

BookDetails.js
import React, { Component } from 'react';
import loadBook from './BookLoader';

class BookDetails extends Component {
  render() {
    // Now "book" comes from props instead of state
    const { book } = this.props;

    return (
      <div>
        <img src={book.coverImg}/>
        <div>{book.author}</div>
        <div>{book.title}</div>
      </div>
    );
  }
}

export default loadBook()(BookDetails);
BookSummary.js
import React, { Component } from 'react';
import loadBook from './BookLoader';

class BookSummary extends Component {
  render() {
    // Now "book" comes from props instead of state
    const { book } = this.props;

    return (
      <div>
        <div>{book.summary}</div>
      </div>
    );
  }
}

export default loadBook()(BookSummary);

4. Simplify (If You Can)

After you finish the HOC refactoring, take a look and see if you can simplify anything further. Sometimes this won’t be possible, but in the case of this example, these components are now simple enough that they can be turned into plain functions. Let’s do that:

BookDetails.js
import loadBook from './BookLoader';

function BookDetails({ book }) {
  return (
    <div>
      <img src={book.coverImg}/>
      <div>{book.author}</div>
      <div>{book.title}</div>
    </div>
  );
}

export default loadBook()(BookDetails);
BookSummary.js
import loadBook from './BookLoader';

function BookSummary({ book }) {
  return (
    <div>
      <div>{book.summary}</div>
    </div>
  );
}

export default loadBook()(BookSummary);

The Finer Details

I wanted to provide a simple example of how to create and incorporate a higher-order component here, so I left out a few details to keep things focused.

displayName

It’s nice to set the displayName property on the HOC so when you look at the element in the React inspector, it’s clear what it is and what it wraps.

Use this function to get the displayName of the WrappedComponent:

function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

Then, in the HOC, right before you return the component, set its displayName:

function loadBook() {
  return function(WrappedComponent) {
    class BookLoader extends React.Component {
      ...
    }

    BookLoader.displayName = `BookLoader(${getDisplayName(WrappedComponent)})`;

    return BookLoader;
  }
}

Composability

Did you notice that the HOC we wrote has a seemingly-unnecessary function nested within it? Why not just write it like this?

function loadBook(WrappedComponent) {
  class BookLoader extends React.Component {
    ...
  }
  return BookLoader;
}

// Used like:
export default loadBook(BookDetails);

// instead of the previous:
export default loadBook()(BookDetails);

There are a couple advantages to the extra function.

  1. There’s a clear place to put extra configuration arguments – in the first function call (like loadBook(config)(BookDetails)). Of course you could pass those to the second call as well, but then you have to decide whether the argument order should be (TheComponent, config) or (config, TheComponent). And it’s also useful because…

  2. When the second function always takes a single component as an argument, and returns a new component, it has the function signature Component => Component. This is useful because you can then “compose” multiple HOCs onto a component, like:

compose(
  loadingIndicator(),
  loadBook(config),
  someOtherHOC()
)(BookDetails);

This compose function is available in a few different libraries, like Redux and Lodash (as lodash.flowRight), and it makes for more readable code than the alternative:

loadingIndicator()(loadBook(config)(someOtherHOC()(BookDetails)))

Wrap Up

Now that you’ve gotten a taste of higher-order components, can you think of any places to apply this in your own code? Give them a try!

After you write a couple HOCs, you’ll start seeing patterns all over that could be refactored this way. Treat it as another tool in your abstraction toolbox to reduce duplicate code.

Drop your email in the box below to get all the source code for this post, as well as a couple exercises to help you practice using HOCs.

comments powered by Disqus