Designing React Components for Modularity, Reusability, and Testability

Michael Jewell
5 min readMar 13, 2017

--

The single responsibility principle states that each component of a system should only do one thing. It’s a pretty widely accepted best practice, but it can be hard to follow at times. By sticking to this principle you can build a system which minimizes the impact of changing requirements, simplifies testing, and reduces the overall complexity of your code base.

In most react examples a single component will handle two broad responsibilities — how it looks, and how it works. This definitely sounds like it’s breaking the single responsibility principle. Fortunately, it’s not too hard to get around this.

Presentational and Container Components

A well-known pattern to separate concerns is to use Presentational and Container Components. You should definitely read that article (and the one it is based off) if you haven’t already, but fundamentally they suggest having a container component that controls the logic, which renders a presentational component that controls the look and feel. This is a great way to design your components, but it does have some problems.

Issues — Testing

In a realistic application we will probably have a pretty large tree of components. It might look something like this:

                                +-> ContainerB -> PresenterB -> ...
/
App -> ContainerA -> PresenterA
\
+-> ContainerC -> PresenterC -> ...

We’re good developers, let’s say we wanted to write some tests. We will start with PresenterA. Using enzyme’s shallow method we can isolate this component and effectively ignore the rest of the tree. We know that we can use shallow instead of mount because we make sure our presenters do not contain any logic beyond presentation. As a result, no matter how complicated the downstream dependencies of PresenterA are, we can effectively ignore them for tests. This is great.

Now we want to test ContainerA. Sometimes we will be able to use shallow, but we can’t guarantee it. There will definitely be times we have to do full DOM rendering with mount and when we do we are forced to render the entire tree below the component under test. This can significantly slow the tests down, but worse than that it can force you to deal with any dependencies of downstream components, like stubbing AJAX calls, even though they’re not really relevant to this particular test. This sucks.

Issues — Reuse

Sometimes our containers have pretty widely applicable logic in them. Let’s say we make a request and while we are waiting we want to render a loading indicator. There’s a pretty good chance that is going to be reused, but since our container is hard-coded to render it’s own presenter component, we have to rewrite this logic each time.

What’s Really the Problem?

At the core of both of the issues is the fact that the presenter is hard coded into the container. Let’s say we have this totally useless container:

class Container extends React.Component {
render() {
return <Presenter {...this.props} />;
}
}

What if we were to inject the presenter component and dynamically create the class instead, like this:

function createContainer(WrappedComponent) {
return class Container extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
}
const Container = createContainer(Presenter);

Now we have a function which returns a container, but we can call it with any component we want. In this case, we also create our old container by calling this function with our presenter. A function like this is called a Higher-Order Component.

Higher-Order Components as Containers

Higher-Order Components (HOCs) are functions which take a react component and return a new component. They are able to control when the passed in component is rendered, and modify, add, or remove any props that are passed in. Containers basically just intelligently pass props down to their presenters, so these should be able to do everything we need.

What Do We Gain?

By doing this we are able to isolate the container component in the test environment and ignore all downstream dependencies. We can easily set up a test like this by passing in some dummy component:

const Dummy = () => <div />;
const ContainerUnderTest = myHigherOrderComponent(Dummy);

Now even if we use mount we know that our test is only dealing with what happens inside the container we are currently testing. We can easily look at its internal state and the props provided to the Dummy component and know that our presenter will receive those props.

You may still want to test the container that renders your presenter, and you still can, exactly how you were before. Those tests have all the downsides listed previously, but hopefully you won’t need as many since you can cover most cases using the dummy component.

We are also able to reuse this container logic in any other parts of our app without having to rewrite it for specific cases. We can wrap any component with this higher-order component without making any modifications and it will receive whatever functionality it provides. By making it easy to reuse these components we encourage building our app out of small components with single responsibilities. Ultimately, this should help us create a well structured app.

What Do We Lose?

As far as I can tell, the only downside to this approach is the added boilerplate of wrapping your components in functions. There is some added complexity in understanding what is going on but the pattern is very consistent regardless of what your containers do, making it pretty easy to learn.

Chaining Higher-Order Components

Sometimes our containers get pretty big, and when they do, they probably handle more than one responsibility. It would be great if we could split any unrelated container logic into different components and use them all with a single presenter. Because these higher-order components just take any component, we can chain these functions together easily:

const Container = HOC1(HOC2(HOC3(Presenter)));
// or
const composedHOC = lodash.flowRight(HOC1, HOC2, HOC3);
const Container = composedHOC(Presenter);

Both of these approaches create a chain of components like this:

-> HOC1 -> HOC2 -> HOC3 -> Presenter

Customizing Higher-Order Components

Sometimes you want to be able to customize how your HOCs work without having to rewrite them. The fact that HOCs take only a single parameter is pretty nice for chaining them, so how can we add other parameters without losing this? Just like before, let’s just wrap it in a function. Wrapping the HOC in another function allows us to provide any additional data we want and still end up with a function which takes only a single parameter, a component to wrap:

function myHigherOrderComponentBuilder(options) {
return function myHigherOrderComponent(WrappedComponent) {
return class extends React.Component {
// we can use `options` in here to configure our container
}
}
}
const myBlueHigherOrderComponent = myHigherOrderComponentBuilder({
color: 'blue'
});
const Container = myBlueHigherOrderComponent(Presenter);

Example

This JSFiddle covers the main uses of this technique and shows a simple presenter being enhanced by HOCs. The example is pretty basic but the patterns described can easily be extended to real-life use cases.

Hopefully it’s clear how using higher-order components can help you build easily testable and reusable container components. In my opinion the benefits heavily outweigh the cost of the small amount of boilerplate required to generate the higher-order components. Although HOCs can seem daunting at first, it’s important to understand that ultimately we created the same container component we would have created normally. This means you can easily try out this method on a single component and see the benefits yourself without having to refactor anything outside of it.

--

--