Mar 31 2021

Separate Concerns with Higher Order Components

Higher order components (commonly known as HoC) are a great tool for separation of concerns. This pattern is highly encouraged in the React world and are commonly found in dependencies such as react-redux. A higher order component can assist in splitting UI from data sources and mutate functionality.

Let’s take a look at an example where we have a to-do list. The data is fetched via an ordinary react hook, and the results are rendered.

function TodoList() {
  const todoItems = useLocalTodoItems();

  return (
      <div>
        {todoItems.map(item => <TodoItem item={item} />)}
    </div>
  )
}

There’s a common saying in the world of software:

A function should do one thing, and one thing well.

You’ve heard of this phrase, right?

Well, this React component is a function. How many concerns are in this function? I’d like to make the argument that it is indeed doing two things, not one.

  1. Fetches the data from some specific source
  2. Renders the items in the user interface

It doesn’t really matter how exactly we are fetching the to-do items. They could be fetched from an API, local storage, etc. What matters is that the fetching and rendering co-exists.

So now let’s split this up!

function TodoList(props: {items: Array<Item>}) {
  return (
      <div>
        {props.items.map(item => <TodoItem item={item} />)}
    </div>
  )
}

function App() {
  const todoItems = useLocalTodoItems();

  return <TodoList items={todoItems} />
}

You might be saying right now: “Jake, this is pedantic. Why would we go through the effort of moving the hook out of <TodoList />?”

My go-to counter argument in this case would be:

  1. If you introduce a nifty tool like Storybook, it’s useful to pass in a list stub for testing UI edge-cases.
  2. A component is more reusable when it has one, simple job. Imagine you’d like to render <TodoList /> in five different places. Would the list always use the same data source? I wouldn’t bet on it. (See #1)
  3. Unit testing <TodoList /> becomes more difficult. How would you test <TodoList /> if it made a request to an API? I am personally not fond of monkey-patching functions during the test runtime. They tend to lead to:
    • mocks clobbering each other when ran asynchronously.
    • return values of the actual function changing and the mock goes stale.

For these reasons, I find dependency injection preferable.

If you take another look at the above code, you might notice that we are shifting “the problem” from one component to another. Now the data-fetching responsibilities are added to the parent component. Some might argue that it is okay if the complexity is being shifted to the root App component. If the application is simple enough, I don’t see the harm in following this.

In some cases, App could be a little more bloated. Let’s maybe look at another potential refactor.

interface TodoListProps { items: Array<Item> };
function TodoList(props: TodoListProps) {
  return (
      <div>
        {props.items.map(item => <TodoItem item={item} />)}
    </div>
  )
}

function withTodoItems(Comp: FC<{items: Array<Item>}>) {
  return (props: {}) => {
    const todoItems = useLocalTodoItems();
    return <Comp items={todoItems} />
  }
}

function App() {
  const List = withTodoItems(TodoList);
  return <List />
}

“Aren’t we just making this unnecessarily complicated?” The answer very-well could be “yes”. Though keep in mind how trivial this application is.

For an application that is quite large, this might be perfectly reasonable. Perhaps the <TodoList /> component is several hundred lines long. Maybe the data fetching function is also just as complicated. This is where programming becomes more of an art than a science. I feel that maintaining separation between data fetching and presentation is worthwhile because it gives breathing room for future features.

More Sources of Data

What if our data fetching logic now needs to support many sources? Suppose that there are three sources: local, homegrown API, and a third party.

interface TodoItems {items: Array<Item>};
function withTodoItemsSource(Comp: FC<TodoItems>, fetchTodoItems: () => Array<Item>) {
  return (props: {items?: Array<Item>}) => {
    const restItems = props.items || [];
    const todoItems = fetchTodoItems();
    return <Comp
      {...props}
      items={[...todoItems, ...restItems]}
    />
  }
}

function withLocalTodoItems(Comp: FC<TodoItems>) {
  return withTodoItemsSource(Comp, useLocalTodoItems);
}

function withAPITodoItems(Comp: FC<TodoItems>) {
  return withTodoItemsSource(Comp, useAPITodoItems);
}

function withThirdPartyTodoItems(Comp: FC<TodoItems>) {
  return withTodoItemsSource(Comp, useThirdPartyTodoItems);
}

function App() {
  const List = withThirdPartyTodoItems(
    withAPITodoItems(
      withLocalTodoItems(
        TodoList
      )
    )
  );

  return <List />
}

The power of the higher order component is realized when we stack these functions; mixing and matching across a nice spectrum. You’ll also notice we earned a good amount of re-usability with the withTodoItemsSource higher order component. Who says a higher order component has to be one layer deep?

Adding Authentication

Perhaps this <TodoList /> should hide behind authentication.

function withAuthentication<T>(Comp: FC<T>) {
  return (props: T) => {
    const isAuthenticated = useAuthentication();
    if (isAuthenticated) {
      return <Comp {...props} />
    } else {
      return <Login />
    }
  }
}

function App() {
  const List = withAuthentication(TodoList);

  return <List items={['one', 'two', 'three']}/>
}

withAuthentication can easily be re-used the rest of the application. Protecting a component behind authentication doesn’t get easier than utilizing a higher order component.

Also, notice the more “meta” detail buried in this example. We are passing our own stubbed list into this component. This demonstrates that our data fetching (withLocalTodoItems, withAPITodoItems, withThirdPartyTodoItems) is decoupled.

An Alternate Reality

I’d typically end the blog post here, but maybe we can take a look at an alternate reality.

What if <App /> didn’t have separation of concerns? What if the higher order components were omitted? Let’s see a more tightly coupled version of the above.

function UnorderlyApp() {
  const localItems = useLocalTodoItems();
  const apiItems = useAPITodoItems();
  const thirdPartyItems = useThirdPartyTodoItems();
  const isAuthenticated = useAuthentication();

  const items = [...localItems, ...apiItems, ...thirdPartyItems];

  return isAuthenticated ? <TodoList items={items} /> : <Login />
}

Let’s see how many concerns the <UnorderlyApp /> contains:

Wow! That’s a lot of coupling. Without a doubt, this issue will only compound overtime as more features are added. Let’s throw another wrench in the situation:

As a user, I would like to disable the third party todo items.

Following this coupled <App />, this would look like:

function UnorderlyApp() {
  const localItems = useLocalTodoItems();
  const apiItems = useAPITodoItems();

  const isThirdPartyEligible = useThirdPartyEligibility();
  let thirdPartyItems = [];
  if (isThirdPartyEligible) {
    thirdPartyItems = useThirdPartyTodoItems();
  }

  const isAuthenticated = useAuthentication();

  const items = [...localItems, ...apiItems, ...thirdPartyItems];

  return isAuthenticated ? <TodoList items={items} /> : <Login />
}

Not only is the complexity growing, but there’s an error in the above code. Can you spot it?

React forbids hooks inside conditionals. This can lead to undefined behavior if a hook is ran in one render cycle, but not the next.

Moving back to the HoC solution, let’s merge the eligibility check with the third party hook.

function withThirdPartyTodoItems(Comp: FC<TodoItems>) {
  return (props: {items?: Array<Item>}) => {
    const isEligible = useThirdPartyEligibility();
    if (isEligible) {
      return withTodoItemsSource(Comp, useThirdPartyTodoItems)(props);
    } else {
      return withTodoItemsSource(Comp, () => [])(props);
    }
  }
}

function App() {
  const List = withAuthentication(
    withThirdPartyTodoItems(
      withAPITodoItems(
        withLocalTodoItems(
          TodoList
        )
      )
    )
  );

  return <List />
}

The augmentation to withThirdPartyTodoItems leaves <App /> untouched. withThirdPartyTodoItems could easily be used many times across the app. By isolating the change to this function, we can safely make this update without touching any of the callers.

My favorite part of using HoC wrapped hooks is that they become easily testable. In fact, there is no need to mock anything (even the third party items). All we would have to do is dependency inject polymorphic behavior. This would even allow you to change third party services on a dime!