Why Containers? (spoiler: not just because it’s cool)

Niole Nelson
Niole Net
Published in
7 min readDec 23, 2018

--

Have you ever written a test? Isn’t testing the worst? Why is it so horrible? Why does it feel impossible to write tests that aren’t terrible to maintain? Why do people hate writing tests? There are quite a few reasons, but right now I’m just going to focus on the horrors of async testing. This is because I am always tempted to write very interactive tests for any UI logic that I write. I just want to test all of the interactions at once and make sure it works, ok? I’m sick of everything breaking all of the time! But it’s never that easy. These half integration half unit tests never work. They always break or my testing framework just doesn’t seem to work the way I want it to and I can’t figure out why. I usually give up.

Not that kind of test

Hooking up async tests is annoying. It’s annoying because no testing framework ever seems to work well with this style of testing. The async tests you write will always be fundamentally flakey. When your testing framework has new versions come out, you won’t be able to use them because they will make all of your async tests fail. That fine tuning that you did a year ago will have been for naught. You will have to either 1. quarantine those tests and have them run separately with their own dependencies, 2. delete them 3. spend a month rewriting them all. Conclusion: no one has time for this. We’re all busy people.

I used to hate testing. Why did I hate it? Once again, async testing. I wanted to test every state of my UI, which meant testing interactions. Ultimately, testing interactions is impossible. Enzyme 2 actually tested interactions relatively well. But enzyme 3 has this problem with React where you have to tell the wrapper to update sometimes. Why is this? Is enzyme no longer being maintained? Can someone give me an explanation for this? Seriously, please comment with your take on this problem and links to supporting evidence. I will deeply appreciate it.

Anyway, yes I used to hate testing. I had lost all hope of every finding the perfect combination of testing tooling and convention to support testing the async interactions that I so deeply want and need to verify in order to maintain some level of sanity.

Is this where we start talking about “why containers”

Why, yes it is.

Now I have a new approach to testing: controlled components. I know, controlled components, containers, et c are pretty old news at this point. But seriously, it’s an oldy but goody. If you get rid of as much async interaction as possible by abstracting it out of your view, then you can test the various static states of your UI and then infer whether or not your component works as intended.

Do you need recompose to create higher order components (HOCs) or some framework to accomplish this?

Technically, no. For an example of a hand spun HOC, take a peek at the `withApiHandlers` container in the code examples at the end of this post.

Let’s quickly define HOC. All of the time I hear people describe containers made with recompose as an HOC. But then I hear those same people become confused if someone uses HOC to describe a component that uses render props to achieve the same separation of concerns.

I must end this confusion for once and for all. Let’s make an analogy to the higher order function. A higher order function is a function that takes another function as an argument. For example, in JavaScript the array method map is a higher order function. It’s a function that takes a function that is then used to project an array of A’s in to and array of B’s.

Given that, what is a higher order component? It’s a component that takes a component as its argument and does something with it. It’s a kind of generic way of composing components to make something happen. I like to say that it’s a way of bootstrapping components in order to give them superpowers that they don’t have on their own.

The bootstrapping power of the HOC is paramount to how it solves our testing problems. Once I removed the async nature from my UI, I saved hours of time which would have been spent on getting those tests just right. My intention was in the right place but I ultimately wasted my energies on creating tests that were destined to break. UI that is broken down in to a series of static states is supportable in the long haul. It makes it way easier to write storybooks for them and unit test them.

One thing that I’ve learned working as a developer over time is that you cannot totally rely on open source projects to help you create a stable product. You’re going to get screwed by something and that is stress that I just don’t need. This is something that you can combat by making smart choices which lead to more maintainable code.

Let’s go through an example of transforming an interactive UI component into a collection of stateless elements that can support a testing story that is maintainable in the long term. Here’s the repo. This is an app that shows politicians tweets. The user clicks on a dropdown to select which politician’s tweets they want to view.

Here’s our “before” implementation of the main view component of this app:

//// mock data
const TWEET_MAP: { [key: string]: string[] } = {
trump: ['Mexico', 'China', 'Kittens'],
obama: ['Change', 'Dreamers', 'Obamacare'],
};
//// view componentclass View extends React.Component<{}, State> {
state = {
selectedPolitician: undefined,
tweets: [],
loading: false,
};
/**
* Both the visualized tweets and the loading state are a product
* of the asynchronous `getPoliticianTweets` operation which is triggered by
* `selectPolitician`
*
* Our refactoring goal is to separate `View` into pieces such that
* this asynchronous operation will be represented in "spirit" but not in
* our tests in practice.
*/
selectPolitician = (value: Politician) => {
const selectedPolitician: Politician = value;
this.setState({ loading: true, selectedPolitician });
getPoliticianTweets(selectedPolitician, (tweets: string[]) => {
this.setState({ loading: false, tweets });
});
}
render() {
const { loading, tweets, selectedPolitician } = this.state;
return (
<MuiThemeProvider>
<Card>
<CardHeader title={<span>Tweets for...
<PoliticianSelector
onSelectPolitician={this.selectPolitician}
selectedPolitician={selectedPolitician}
/>
</span>}
/>
<CardText>
{loading ? <Loader /> : tweets.map((tweet: string) => <div key={tweet}>{tweet}</div>)}
</CardText>
</Card>
</MuiThemeProvider>
);
}
}

If we left the implementation like this, in test, we would have to mock the `getPoliticianTweets` API call, which is easy enough but that’s not what’s hard. What’s hard is finding the right elements to simulate clicks on, hoping that you can get the timing just right with your assertions, and also the fact that sometimes UI component libraries for React access the DOM in ways that your testing library doesn’t deal with gracefully. If you factor your UI logic into stateless components and use containers to handle the asynchrony, then your testing story becomes much simpler.

The following is an example of how you could refactor your code to achieve that:

type StateProps = {
selectedPolitician?: Politician;
tweets: string[];
loading: boolean;
};
type HandlerProps = {
onSelectPolitician: (value?: any) => void;
};
type Props = StateProps & HandlerProps;//// the view/**
* View is now a stateless react element, whose different states are much easier to test than before.
*/
type TweetView = React.SFC<Props>;
export const View: TweetView = ({ onSelectPolitician, loading, tweets, selectedPolitician }) => (
<Card>
<CardHeader title={<span>Tweets for...
<PoliticianSelector
onSelectPolitician={onSelectPolitician}
selectedPolitician={selectedPolitician}
/>
</span>}
/>
<CardText>
{loading ? <Loader /> : tweets.map((tweet: string) => <div key={tweet}>{tweet}</div>)}
</CardText>
</Card>
);
/// the containerconst withApiHandlers = (Component: TweetView) => class extends React.Component<{}, StateProps> {
state = {
selectedPolitician: undefined,
tweets: [],
loading: false,
};
/**
* withApiHandlers contains the state that was once in View and now drives all asynchronous
* interactions.
*/
selectPolitician = (value: Politician) => {
const selectedPolitician: Politician = value;
this.setState({ loading: true, selectedPolitician });
getPoliticianTweets(selectedPolitician, (tweets: string[]) => {
this.setState({ loading: false, tweets });
});
}
render() {
const { loading, tweets, selectedPolitician } = this.state;
return (
<Component
loading={loading}
tweets={tweets}
selectedPolitician={selectedPolitician}
onSelectPolitician={this.selectPolitician}
/>
);
}
};
/// wrapping the view in with the containerexport default withApiHandlers(View);

Now that the core UI logic has been factored into a stateless React element, the UI that it outputs is literally a function of its inputs. You can express all of its states in a suite of tests quite easily. The tests will be stable and quite easy to write. Please check out the tests I wrote for this view logic here.

Oh and before you go, here’s a picture of the app in action:

My finest work to date
Such beautiful loading dots
#material-ui #frontendgenious

--

--