How to use container components without sacrificing testability
If you’ve built an app using React and Redux, you’ve probably heard of the concept of separating presentational components and container components, or smart components and dumb components, or similar terminology.
If you haven’t, you might want to read this excellent summary about them by the legendary Dan Abramov.
One thing that I struggled with for a long time as a front-end developer, though, is when to draw the line. Many people have varying opinions on this, but in general the consensus is this:
Use few, high-level container components, which render presentational components. The reason for this is because presentational components are more reusable (they are not coupled to a specific slice of the store), more easily testable (the tests don’t require mocking an entire store just to stub in some data), and readable (they have relatively little logic and mostly are just configurable and reusable blocks of JSX).
This is good advice in theory, but it doesn’t feel like a very concrete rule. The reality is, in a larger React application, you will have dozens of levels of component nesting. If a child component is a container, then testing and reusing the parent component becomes more challenging. But by contrast, if we only make high-up parent components containers, then we have to pass props down many levels of nesting to the child presentational components.
Most of the time, we want to use react-redux’s connect
directly on the component that needs to read store data and dispatch actions. The farther removed we are from the component that needs to access the store, the more confusing it becomes as to where the data comes from, and the more likely we are to miss a step and fail to pass down the correct props. But once you embed a container component inside a presentational component, haven’t we made the parent presentational component no longer presentational?
What really helped me to understand how to solve this problem was to stop thinking of components as either presentational or container, and start seeing basically all components as presentational.
My approach to building a new component is to maximize its reusability. Imagine every new component as if you were building an open source module. For example, consider this component:
const TodoList = ({ tasks, onComplete }) => (
<ul className="list">
{tasks.map(({ id, content }) => (
<li key={id} className="list-item">
{content}
<button onClick={() => onComplete(id)}>×</button>
</li>
))}
</ul>
)TodoList.propTypes = {
tasks: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
})
).isRequired,
onComplete: PropTypes.func.isRequired
}
Naturally, this component is very easy to connect to Redux:
const TodoListContainer = connect(
state => state.todos,
{
onComplete: id => ({ type: COMPLETE_TASK, payload: { id } }),
}
)(TodoList)
The downside is, we’ve created a one-off component. Yes, technically we’ve separated the component from the container. Someone could reuse this TodoList
component, because its props are fairly simple. But this is really a component for rendering a list of things, and it’s being unnecessarily coupled to to-dos.
What makes more sense is to create a List
component; however, this example feels contrived, because a list is so simple it’s not really necessary to make a component out of it.
Let’s look at a more realistic example of when a component is unnecessarily coupled to the store:
const ProductList = ({
products,
sort,
onSortChange,
categories,
categoryFilter,
onCategoryFilterChange,
}) => {
const handleSortChange = event => {
const [property, direction] = event.target.value.split('_')
onSortChange({ property, direction })
} const handleCategoryFilterChange = event => (
[...event.target.options]
.filter(o => o.selected)
.map(o => o.value)
) const productsToRender = sortBy(
products.filter(product => (
categoryFilter.includes(product.categoryId)
),
sort.property
)
if (sort.direction === 'desc') {
productsToRender.reverse()
} const categoriesById = keyBy(categories, 'id') return (
<div>
<h1>Products</h1>
<div>
<label>
Sort:
<select
value={`${sort.property}_${sort.direction}`}
onChange={onSortChange}
>
<option value="name_asc">Name, ascending</option>
<option value="name_desc">Name, descending</option>
<option value="price_asc">Price, ascending</option>
<option value="price_desc">Price, descending</option>
</select>
</label>
<label>
Categories:
<select
multiple
value={categoryFilter}
onChange={handleCategoryFilterChange}
>
{categories.map(({ id, name }) => (
<option key={id} value={id}>{name}</option>
))}
</select>
</label>
</div>
<ul>
{products.map(({ id, name, price, categoryId }) => (
<li key={id}>
<h2>{name}</h2>
<h3>${price}</h3>
<p>Category: {categoriesById[categoryId].name}</p>
</li>
))}
</ul>
</div>
)
}ProductList.propTypes = {
products: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
price: PropTypes.number.isRequired,
categoryId: PropTypes.string.isRequired,
})
).isRequired,
sort: PropTypes.shape({
property: PropTypes.string.isRequired,
direction: PropTypes.oneOf(['asc', 'desc']).isRequired,
}),
onSortChange: PropTypes.func.isRequired,
categories: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
})
).isRequired,
categoryFilter: PropTypes.arrayOf(PropTypes.string).isRequired,
onCategoryFilterChange: PropTypes.func.isRequired,
}
This component is doing a lot, and it probably makes sense to break it down into smaller components. Like the previous component, it is easy to create a container from:
const ProductListContainer = connect(
state => ({
products: state.products,
sort: state.sort,
categories: state.categories,
categoryFilter: state.categoryFilter,
}),
{
onSortChange: sort => ({
type: UPDATE_SORT,
payload: { sort },
}),
onCategoryFilterChange: categoryIds => ({
type: UPDATE_CATEGORY_FILTER,
payload: { categoryIds },
}),
}
)(ProductList)
Your instinct might be to just break this component down into smaller pieces. For example, maybe the sort and filter sections could be in their own component, and the product list could be in another. That begs the question of whether the parent component should be connected and have to pass down the appropriate props, or if the child components should be connected.
The size or complexity of the component, however, is just a distraction. Breaking it down into smaller pieces might be a good idea, but it doesn’t solve our problem.
In fact, if we were to break this component up, there is little point in not making all of these components containers, because they’re already coupled to the store! Do we honestly expect the ProductsList
component to ever be used in another context? The props of this component are the shape of the store: an array of products with id
, name
, price
, and categoryId
; a sort object with property
and direction
; an array of categories with id
and name
; an array of category ids to filter the products by. This component will never realistically be used in another area of the app, or in another app. It allowed the store to dictate its design.
Instead, let’s completely change our approach and build the component without the store in mind. Instead, we will let the props flow naturally from the component’s UI rather than the data it receives.
Let’s consider this beautiful mock you just received from me, an excellent designer:
Without any consideration of the store, we can imagine how best to break down this interface into components. Doing so doesn’t require that much thought; your components become more or less just collections of HTML markup organized into reusable chunks.
Try to remove as much context as you can! Look more at how the content is presented and as little as possible about what the data is. It doesn’t matter that this is a list of products; it really is a layout that includes some pieces:
- A form with some fields
- Each field has a label and an input (
SelectField
) - A list of things (
List
) - Each thing has some headers and a paragraph (
Card
)
We can break down the pieces which are sufficiently generic:
// Render a label and select dropdown
const SelectField = ({
label,
value,
options,
onChange,
multiple,
}) => {
const handleChange = event => {
const selectedOptions = [...event.target.options]
.filter(option => option.selected)
.map(option => option.value)
return multiple ? selectedOptions : selectedOptions[0]
} return (
<label>
{label}:
<select value={value} onChange={onChange} multiple={multiple}>
{options.map(({ value, label }) => (
<option key={value} value={value}>{label}</option>
))}
</select>
</label>
)
}SelectField.propTypes = {
label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
options: PropTypes.arrayOf(
PropTypes.shape({
value: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
})
).isRequired,
onChange: PropTypes.func.isRequired,
multiple: PropTypes.bool
}SelectField.defaultProps = {
multiple: false,
}// Generic component for rendering an array of items as an
// unordered list
const List = ({ items, ItemComponent, itemKey }) => (
<ul>
{items.map((item, i) => (
<li key={itemKey(item, i)}>
<ItemComponent {...item} />
</li>
))}
</ul>
)List.propTypes = {
items: PropTypes.array.isRequired,
ItemComponent: PropTypes.func.isRequired,
itemKey: PropTypes.func,
}List.defaultProps = {
itemKey: (item, i) => i,
}// Renders a short block of content with a title and subtitle
const Card = ({ title, subtitle, children }) => (
<div>
<h2>{title}</h2>
{subtitle && <h3>{subtitle}</h3>}
{children}
</div>
)Card.propTypes = {
title: PropTypes.node.isRequired,
subtitle: PropTypes.node,
children: PropTypes.node.isRequired,
}Card.defaultProps = {
subtitle: null,
}
All of these components are highly reusable, configurable, and trivial to test. We’ve done some preparation in advance, like setting up the List
component to accept an arbitrary function for rendering the list items (in the future, this list might render products, or anything else).
We still need to write some components; however, the remaining pieces are very specific to the product list, so it’s totally okay that they’re containers!
Our remaining components will be specific to a list of products; it makes sense, then, that they will be containers. They will simply glue our more generic components together and pass them specific configurations.
const ProductFiltersForm = ({
sort,
categories,
categoryFilter,
onSortChange,
onCategoryFilterChange,
}) => (
<div>
<SelectField
label="Sort"
value={`${sort.property}_${sort.direction}`}
options={[
{ value: 'name_asc', label: 'Name, ascending' },
{ value: 'name_desc', label: 'Name, descending' },
{ value: 'price_asc', label: 'Price, ascending' },
{ value: 'price_desc', label: 'Price, descending' },
]}
onChange={value => {
const [property, direction] = value.split('_')
onSortChange({ property, direction })
}}
/>
<SelectField
label="Categories"
value={categoryFilter}
options={categories.map(({ id, name }) => ({
value: id,
label: name,
}))}
onChange={onCategoryFilterChange}
multiple
/>
</div>
)const ProductFiltersFormContainer = connect(
state => ({
sort: state.sort,
categories: state.categories,
categoryFilter: state.categoryFilter,
}),
{
onSortChange: sort => ({
type: UPDATE_SORT,
payload: { sort },
}),
onCategoryFilterChange: categoryIds => ({
type: UPDATE_CATEGORY_FILTER,
payload: { categoryIds },
}),
}
)(ProductFiltersForm)
This is a good start, but there’s some logic in this component. We want to make it as simple as possible, so that we can minimize what can go wrong in this component, given that it’s hard to test.
Let’s introduce reselect, which allows us to translate the Redux store state before passing it into the component.
const ProductFiltersForm = ({
sortValue,
categoryOptions,
categoryValue,
onSortChange,
onCategoryFilterChange,
}) => (
<div>
<SelectField
label="Sort"
value={sortValue}
options={[
{ value: 'name_asc', label: 'Name, ascending' },
{ value: 'name_desc', label: 'Name, descending' },
{ value: 'price_asc', label: 'Price, ascending' },
{ value: 'price_desc', label: 'Price, descending' },
]}
onChange={onSortChange}
/>
<SelectField
label="Categories"
value={categoryValue}
options={categoryOptions}
onChange={onCategoryFilterChange}
multiple
/>
</div>
)const sortValueSelector = createSelector(
[state => state.sort],
sort => `${sort.property}_${sort.direction}`
)const categoryOptionsSelector = createSelector(
[state => state.categories],
categories => categories.map(({ id, name }) => ({
value: id,
label: name,
}))
)const ProductFiltersFormContainer = connect(
state => ({
sortValue: sortValueSelector(state),
categoryOptions: categoryOptionsSelector(state),
categoryValue: state.categoryFilter,
}),
{
onSortChange: value => {
const [property, direction] = value.split('_')
return {
type: UPDATE_SORT,
payload: {
sort: { property, direction },
},
}
},
onCategoryFilterChange: categoryIds => ({
type: UPDATE_CATEGORY_FILTER,
payload: { categoryIds },
}),
}
)(ProductFiltersForm)
Here we’ve moved the logic from the component to the connect
call. As mentioned, one advantage is that the use of reselect helps to optimize: the translation only happens when the relevant store state changes, instead of every time the component renders. In addition, it’s easy to test these selectors (as well as to export mapStateToProps
so it can be tested as well), because these are just simple, pure functions that accept an object and return another object.
As you can see, the form is a container; by proxy, if we embed it in another component, then that component is also a container, because the parent wouldn’t be renderable without the store. But this is okay! The meat of all of these container components is just to wrap our presentational components. Yes, they aren’t reusable, but there’s just not much in them to reuse in the first place. We aren’t violating DRY principles, nor are we writing a whole lot of code that’s hard to test in isolation.
Let’s make the list component as well. We’ll leverage selectors again, this time to do the product filtering and sorting and adding the category info, rather than cluttering up the component with that logic:
const ProductListItem = ({ title, subtitle, description }) => (
<Card title={title} subtitle={subtitle}>
{children}
</Card>
)const getProductKey = product => product.idconst ProductList = ({ products }) => (
<List
items={products}
ItemComponent={ProductListItem}
itemKey={getProductKey}
/>
)const productItemsSelector = createSelector(
[
state => state.products,
state => state.categories,
state => state.sort,
state => state.categoryFilter,
],
(products, categories, sort, categoryFilter) => {
const items = sortBy(
products.filter(product => (
categoryFilter.includes(product.categoryId)
),
sort.property
)
if (sort.direction === 'desc') {
items.reverse()
} const categoriesById = keyBy(categories, 'id')
return items.map(({ id, name, price, categoryId }) => ({
id,
title: name,
subtitle: `$${price}`,
children: `Category: ${categoriesById[categoryId].name}`,
}))
}
)const ProductListContainer = connect(
state => ({
products: productItemsSelector(state),
})
)(ProductList)
Almost no JSX at all!
Actually, you might notice that ProductListItem
is totally unnecessary; since we’ve already translated the product data into the props accepted by the Card
component in our selector, this component is just a redundant wrapper for Card
.
In fact, with a little bit of cleverness in the mergeProps
function of connect
, we can actually completely remove all JSX from these containers!
const getProductKey = product => product.idconst productItemsSelector = createSelector(
[
state => state.products,
state => state.categories,
state => state.sort,
state => state.categoryFilter,
],
(products, categories, sort, categoryFilter) => {
const items = sortBy(
products.filter(product => (
categoryFilter.includes(product.categoryId)
),
sort.property
)
if (sort.direction === 'desc') {
items.reverse()
} const categoriesById = keyBy(categories, 'id')
return items.map(({ id, name, price, categoryId }) => ({
id,
title: name,
subtitle: `$${price}`,
children: `Category: ${categoriesById[categoryId].name}`,
}))
}
)const ProductListContainer = connect(
state => ({
items: productItemsSelector(state),
}),
null,
((stateProps, dispatchProps, ownProps) => ({
...stateProps,
...dispatchProps,
...ownProps,
ItemComponent: Card,
itemKey: getProductKey,
})
)(List)
Now, before we get ahead of ourselves, this isn’t always the practical solution. Sometimes writing one-off components for a container is cleaner than trying to, say, generate some JSX children inside a selector. Never be a perfectionist at the cost of code readability; in fact, this example even is a bit contrived: it might be better to keep ProductListItem
around and have it translate a product object to a Card
’s props than it would to do such a thing in a selector. In a practical example, we might have done more complex custom rendering, such as a product thumbnail, a hyperlink, and other specific markup for formatting the item.
Nevertheless, if you strive to make simple, reusable components and wrap more translation logic in connect
and selectors, the result is the best of both worlds: ultra-reusable and testable components, and lightweight containers that wrap them.
Putting it all together, we have a final component that wraps our form and list:
const ProductSectionContainer = () => (
<div>
<ProductFiltersFormContainer />
<ProductListContainer />
</div>
)
I would consider this a container and would put it in the appropriate directory, conform to the appropriate naming convention, etc. This would simply promote that, even though this component itself is not connected to the store, that it cannot be reused or rendered without being inside of a Provider
, because its children are containers. But don’t sweat it, what about this component needs testing or reusability?
At most, its complexity will be an arrangement of its children, and where they are positioned in a CSS-controlled layout.
In summary:
- Model your components around the UI and try to ignore the shape of data (and even the type of entity) that it will eventually render. Try to identify the most generic bits so you can maximize reuse. When your components are based on their appearance and not on what they show, you’ll be surprised at just how much of your app is composed of the same bits and pieces. Imagine your presentational components as just fancy new HTML elements: an
img
is an image, not a profile pic or product thumbnail. - Leverage react-redux and reselect’s capabilities to mold your store’s data to fit your component, rather than have your component do it. Limit your component to template-based logic and keep it JSX-heavy.
- Don’t be afraid to use
connect
where it makes sense, just to avoid having “too many containers.” If that philosophy held true, then the only logical conclusion would be that only your root component can be a container, and that would defeat the point. Just make sure the parent components are simple and you have nothing to worry about! - Remember, every function you write can be named and exported. So export your
mapStateToProps
, your selectors, and your utility functions, so they can be imported in a test suite. It’s much, much easier to test a pure component that translates one object into another, than it is to test the output of an intricate React component thoroughly.