If you've been tinkering with ReactJS or learning about it online, you've likely come across the concept of Container Components.
Originally posted by Jack Preston at unwttng.com
What?
If not, there's a great little primer as part of CSS-Tricks' great Leveling Up With React series, but I'll briefly describe it here too, for the sake of having an established code example.
The point is that rather than having a single stateful React component C
(definition: "a React component which makes use of and updates its state rather than, say, relying entirely on props), you split C
into two components, let's call them CContainer
and CView
.
This split allows you to separate concerns between the two. CContainer
handles state management pertaining to app logic in response to user events, makes appropriate API calls for fresh data, that sort of thing. CView
is the sole content of CContainer
's render method, and defines the actual markup of the component. It's passed any data it needs as props by CContainer
and should therefore be stateless (so says the standing argument, but we'll get to that).
Let's write a simple example (skipping imports, error handling etc for brevity):
const ProfileContainer = React.createClass({
getInitialState() {
return { profile: null }
},
componentDidMount() {
someAjaxLib
.get('/api/my/profile')
.then(profile => this.setState({ profile }))
},
render() {
return (<ProfileView
username={this.state.profile.username}
profileCreated={this.state.profile.created} />)
},
})
This component has a profile
state property, and on mount it fires off an API request for that data. On response, it updates its state. The profile data that comes back is expected to look like:
{
username: 'something',
created: 1474276030847, // Epoch timestamp
}
To render, though, all it does is delegate to ProfileView
:
function ProfileView(props) {
return (
<div>
<p>Your username is {props.username}</p>
<p>Your profile was created at {props.profileCreated}</p>
</div>
)
}
The Container Component model is such a de facto standard that you can see above I've been able to define the ProfileView
in an abbreviated form. It's not React.createClass()
'd, but rather is just a single function taking props. This, fairly, formalises the idea that a view component should handle props only and, above all, be stateless.
Stateful views
Having said all of that, I frequently write stateful view components. I don't write combined components: I'm still making the container / view split when I do this. It's just that they both have state.
And here's my justification: some views require driving data which has nothing to do with component logic.
I'll introduce a change to the above example: instead of showing the user the timestamp at which their profile was created, we now want to show them how old their profile is in seconds. And, naturally, we want that count to update in real time (this isn't a completely contrived example: the same thing is done on a tens-of-milliseconds basis on my image-saving service voolt.io).
This doesn't, and rightly shouldn't in my opinion, require a single change to ProfileContainer
. However, we have to change ProfileView
. Importantly, we have to make it stateful:
const ProfileView = React.createClass({
getInitialState() {
return { now: new Date().getTime() }
},
componentDidMount() {
setInterval(() => this.setState({ now: new Date().getTime() }), 1000)
},
render() {
return (
<div>
<p>Your username is {props.username}</p>
<p>Your profile was created {(this.state.now - props.profileCreated) / 1000} seconds ago</p>
</div>
)
},
})
We can't simply write {(new Date().getTime() - props.profileCreated) / 1000}
directly in the render method, since React provides no way (again, rightly) to just force a re-render every second regardless of a lack or props or state changes. So we need something like my now
state.
But should now
really be a part of ProfileContainer
's state? I don't think so - to me, the situation we have now is the ideal one. ProfileContainer
holds information actually pertaining to the profile, and ProfileView
also holds information - this time about something important only to the visual representation of the data.
I see this pattern being useful all over the place - as another example, consider a rich image component which displays a loading percentage while the browser downloads the data. The container can hold information on the actual image URL, whereas the view can perform a background preload and respond to onDownloadProgress
events or the like to render a percentage.
Know where you stand
That's not to say I never use stateless components. I do, all the time, and I often make use of the handy functional definition of these components. But zealously protecting the statelessness of view components is a misguided fight, in my opinion. By all means zealously protect the role of the state both sides keep track of: meaty, app-centric data in the container and ephemeral, view-centric data in the view. Above all be flexible to the fact that you separated concerns for a reason. Lumping view-relevant state in the container takes you back a step from that (constructive) separation.
Have any thoughts?