Presentational and Container Components in React-Redux
Bài đăng này đã không được cập nhật trong 3 năm
As all of my study reports so far have made me seem like an AngularJS maniac, I have decided to pick a new chicken soup for my soul: React-Redux (well, actually, the main reason is that I have to deal with this bestie couple currently in my project now and trust me, they turned out not be as friendly as the rumor said at first and had been tormented my soul with their taste of chicken-soup-which-is-full-of-bones. Luckily, I have gradually learned to get along with these fellows... (godblessme)
Today, we'll explore a new paradigm for organizing our React components, which can be divided into two types: presentational components and container components. We'll see how doing so limits knowledge of our Redux store to container components as well as provides us with flexible and re-usable presentational components.
1. Concept
In React, a presentational component is a component that just renders HTML. The component's only function is presentational markup. In a Redux-powered app, a presentational component does not interact with the Redux store.
The presentational component accepts props from a container component. The container component specifies the data a presentational component should render and the behaviors as well. If the presentational component has any interactivity — like a button — it calls a prop-function given to it by the container component. In other words, the container component is the one to dispatch an action to the Redux store:
Let's take a look at the ThreadTabs
component:
class ThreadTabs extends React.Component {
handleClick(id) {
store.dispatch({
type: 'OPEN_THREAD',
id: id,
})
}
render() {
const { tabs } = this.props
const tabList = tabs.map((tab, index) => (
<div
key={index}
className={tab.active ? 'active item' : 'item'}
onClick={(ev) => this.handleClick(tab.id)}
>
{tab.title}
</div>
))
return (
<div className='ui top attached tabular menu'>
{tabList}
</div>
)
}
}
At the moment, this component both renders HTML (the tab
div) and communicates with the store. It dispatches the OPEN_THREAD
action whenever a tab is clicked.
But what if one day, the requirement changes and there are some new kinds of tab appearing in your app? These other kinds of tab might probably have to dispatch another type of action. So what we have to do is to write an entirely different component with distinguished name like <ATab /> and <BTab> even though the HTML they renders are just the same. And you keep going with this tedious task if C, D, E, F Tab show up in a near future....
How about creating a generic tabs component, let just say, Tabs
? This presentational component would not specify what kind of behavior is executed when users click on a tab. Instead, we could wrap it in a container component wherever we want this particular markup in our app. That container component could then specify what action to take by dispatching to the store.
We'll call our container component ThreadTabs
. It will be in charge of the communicating with the store and let Tabs
handle the markup. In the future, in case we want to use tabs elsewhere, for example, in a "contacts" view that has a tab for each group of contacts — we could re-use our presentational component:
2. Splitting up
We'll split up ThreadTabs
by first writing the presentational component Tabs
. This component will only be concerned with rendering the HTML — the array of horizontal tabs. It will also receive a prop, onClick
. The presentational component will allow its container component to specify whatever behavior it wants when a tab is clicked.
Let's add Tabs
. Write it above the current ThreadTab
component. The JSX for the HTML markup is the same as before:
const Tabs = ({ tabs, onClick }) => (
<div className='ui top attached tabular menu'>
{
tabs.map((tab, index) => (
<div
key={index}
className={tab.active ? 'active item' : 'item'}
onClick={(ev) => onClick(tab.id)}
>
{tab.title}
</div>
))
}
</div>
)
React allows us to declare stateless components. Stateless components, like Tabs
, are just JavaScript functions that return markup. They are not special React objects.
Because Tabs
does not need any of React's component methods, it can be a stateless component (only receiving props and no state).
In fact, all our presentational components can be stateless components. This reinforces their single responsibility of rendering markup. It is recommended by the React core team that we use stateless components whenever possible.
As we can see, the first argument passed in to a stateless component is props
which is an object with two attributes tabs
and onClick
. Because Tabs
is not a React component object, it does not have the special property this.props
. Instead, parents pass props to stateless components as an argument.
Our presentational component is ready. Let's see what the container component that uses it looks like. Modify the current ThreadTabs
component:
class ThreadTabs extends React.Component {
render() {
const { tabs } = this.props
return (
<Tabs
tabs={tabs}
onClick={(id) => (
store.dispatch({
type: 'OPEN_THREAD',
id,
})
)}
/>
)
}
}
Our container component specifies the props and behavior for our presentational component. We set the prop tabs
to this.props.tabs
, specified by App
. Next, we set the prop onClick
to a function that calls store.dispatch()
. We expect Tabs
to pass the id of the clicked tab to this function.
If we were to test the app out now, we'd be happy to note that our new container/presentational component combination is working.
However, there's one odd thing about ThreadTabs
: It sends actions to the store directly with dispatch, yet at the moment it's reading from the store indirectly through props (through this.props.tabs
). App
is the one reading from the store and this data trickles down to ThreadTab
s. But if ThreadTabs
is dispatching directly to the store, is this indirection for reading from the store necessary?
Instead, we can have all of our container components be responsible for both sending actions to the store and reading from it by subscribing directly to the store in componentDidMount
, the same way that App
does:
class ThreadTabs extends React.Component {
componentDidMount() {
store.subscribe(() => this.forceUpdate())
}
Then, inside of render
, we can read state.threads
directly from the store with getState()
. We'll generate tabs here using the same logic that we used in App
:
render() {
//get state of redux
const state = store.getState()
//the array "threads" in redux state for storing data of thread list
const tabs = state.threads.map(t => ({
title: t.title,
active: t.id === state.activeThreadId,
id: t.id,
})
)
Now we don't need to read from this.props
at all. We pass Tabs
the tabs variable that we created:
return (
<Tabs
tabs={tabs}
onClick={(id) => (
store.dispatch({
type: 'OPEN_THREAD',
id,
})
)}
/>
)
Our Tabs
component is purely presentationalt specifies no behavior of its own.
The ThreadTabs
component is a container component. It renders no markup. Instead, it interfaces with the store and specifies which presentational component to render. The container component is the connector of the store to the presentational component.
Our presentational and container component combination, in full:
const Tabs = ({ tabs, onClick }) => (
<div className='ui top attached tabular menu'>
{
tabs.map((tab, index) => (
<div
key={index}
className={tab.active ? 'active item' : 'item'}
onClick={() => onClick(tab.id)}
>
{tab.title}
</div>
))
}
</div>
)
class ThreadTabs extends React.Component {
componentDidMount() {
store.subscribe(() => this.forceUpdate())
}
render() {
const state = store.getState()
const tabs = state.threads.map(t => (
{
title: t.title,
active: t.id === state.activeThreadId,
id: t.id,
}
))
return (
<Tabs
tabs={tabs}
onClick={(id) => (
store.dispatch({
type: 'OPEN_THREAD',
id: id,
})
)}
/>
)
}
}
In addition to the ability to re-use our presentational component elsewhere in the app, this paradigm gives us another significant benefit: We've de-coupled our presentational view code entirely from our state and its actions. As we'll see, this approach isolates all knowledge of Redux and our store to our app's container components. This minimizes the switching costs in the future. If we wanted to move our app to another state management paradigm, we wouldn't need to touch any of our app's presentational components.
3. Reference Links
ECMAScript 6 — New Features: Overview & Comparison React - A Javascaript library for building user interfaces Redux
All rights reserved