+1

Presentational and Container Components in React-Redux

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 ThreadTabs. 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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí