Container/presentational component pattern - and how it can be replaced with hooks
Last updated on January 2020
We often end up with huge apps that mutate state, run business logic, make API calls, and manage the UI rendering. This can sometimes be too much, with blurry separation of concerns. One pattern to address this is the container/presentational pattern - although I believe now it can be replaced with hooks.
Introduction to the container/presentational pattern
This is a well known pattern, but the container/presentational pattern is also known as the smart/dumb or skinny/smart pattern.
A common pattern seen in React (and Vue) applications is the container/presentational pattern (also known as smart/dumb). Even if you are not familiar with the name, you have almost definitely seen/implemented versions of this in your apps.
I'll go through it with a very simple example... I'll use an example with JSX / react, but the idea is the same for Vue apps.
I'll keep referring to "business logic" but my example is so simple there is none, except grabbing data from an API.
// one single component doing everything:
function LatestPosts() {
// state, business logic:
const [posts, setPosts] = React.useState([])
useEffect(() => {
// basic example for demo purposes
yourApi.getPosts().then(posts => setPosts(posts))
})
// UI:
return <div>
<h1>Your posts:</h1>
{posts.map(post => <div key={post.id}>{post.title}</div>)}
</div>
}
Imagine we had that previous component. It is a very simple example for this blog post, but you probably see versions on it in your apps.
- Some state logic
- Some logic to communicate with outside sources (API endpoint)
- and rendering logic.
Example of a container/presentational component:
Converting this to container/presentational would look something like this:
// container component:
function LatestPostsContainer() {
// state/business logic
const [posts, setPosts] = React.useState([])
useEffect(() => {
yourApi.getPosts().then(posts => setPosts(posts))
})
// pass it down to presentational component:
return <LatestPosts posts={posts} />
}
// presentatioal component:
function LatestPosts(props) {
// stateless function, only dealing with UI
// and uses props
return <div>
<h1>Your posts:</h1>
{props.posts.map(post => <div key={post.id}>{post.title}</div>)}
</div>
}
Ok, so what is so special about it?
On the face of it there is not much different.
But now we have a clear separation:
- container is where we can run business logic, update state, handle events
- and the presentational which should normally be stateless and is concerned only with the UI.
Advantages of container/presentational (aka smart/dumb)
For larger apps this can make it easier to manage your application's state/business logic while decoupling it with the UI (presentational).
Its (almost) always a good idea to have nice boundaries and separation of concerns, and splitting up business logic (and state) with the UI is a good example.
Testing is also easier as you can test the business logic with a mocked UI (presentational), and test the UI by just passing in expected props.
It can also mean more team members can easier work on different things without hitting as many merge conflicts (although I think this shouldn't be a primary reason for adopting this model).
You can often also find ways to easily reuse the presentational components.
Or you can have multiple presentational components, for example:
function LatestPostsContainer() {
// business logic/state:
const [posts, setPosts] = React.useState([])
const [isLoading, setIsLoading] = React.useState(false)
useEffect(() => {
yourApi.getPosts()
.then(posts => setPosts(posts))
.finally(() => setIsLoading(false))
})
// render different presentational components
// depending on the current state:
if(isLoading)
return <LoadingLatestPosts />
if(posts.length === 0)
return <LatestPostsEmpty />
return <LatestPosts posts={posts} />
}
How it can be replaced with custom hooks
Now that we have hooks (composition API in Vue), we can sometimes split it up like this:
- Have a single component, which is (mostly) concerned about UI
- But that component uses a custom hook (such as
usePosts()
) that can handle all of your business logic and state.
Then we can easily modify and test the hook in isolation, and keep the component almost entirely focused on the UI.
We can also easily test the logic in the hook, or test just the UI by mocking the hook.
Following on from the basic example above, converting it to use custom hooks would look something like:
// custom hook
function usePosts() {
const [posts, setPosts] = React.useState([])
useEffect(() => {
yourApi.getPosts().then(posts => setPosts(posts)
})
return posts
}
//
function LatestPosts() {
const posts = usePosts()
return <div>
<h1>Your posts:</h1>
{posts.map(post => <div key={post.id}>{post.title}</div>)}
</div>
}
In this very simple example we can now easily test a lot of the business logic (in the hook) without worrying about the UI.
Although if testing it like that, its going to be testing implementation details and I think should be avoided if possible, but sometimes its just much easier to test a small unit (in this case - the hook) like this.