Container 與 Presentational Components 入門

前言

在聊完了 React 和 Redux 整合後我們來談談分離 Presentational 和 Container Component 的概念,若你是第一次聽過這個名詞,我建議你可以先看看 Redux 作者 Dan AbramovFollow 所寫的這篇文章 @dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.vtcuxsurv">Presentational and Container Components。

Container 與 Presentational Components 超級比一比

以下先參考 Redux 官網 列出兩者相異之處:

  1. Presentational Components

    • 用途:怎麼看事情(Markup、外觀)
    • 是否讓 Redux 意識到:否
    • 取得資料方式:從 props 取得
    • 改變資料方式:從 props 去呼叫 callback function
      • 寫入方式:手動處理
  2. Container Components

    • 用途:怎麼做事情(擷取資料,更新 State)
    • 是否讓 Redux 意識到:是
    • 取得資料方式:訂閱 Redux State(store)
    • 改變資料方式:Dispatch Redux Action
    • 寫入方式:從 React Redux 產生

    從上面的分析讀者可以發現,兩者最大的差別在於 Component 主要負責單純的 UI 的渲染,而 Container 則負責和 Redux 的 store 溝通,作為 ReduxComponent 之間的橋樑。這樣的分法可以讓程式架構和職責更清楚,所以接下來我們就使用上一章節的 Redux TodoApp 進行改造,改造成 Container 與 Presentational Components 模式。

Container Components

以下是 src/containers/TodoHeaderContainer/TodoHeaderContainer.js 的部份:

  1. import { connect } from 'react-redux';
  2. import TodoHeader from '../../components/TodoHeader';
  3. // 將欲使用的 actions 引入
  4. import {
  5. changeText,
  6. createTodo,
  7. } from '../../actions';
  8. const mapStateToProps = (state) => ({
  9. // 從 store 取得 todo state
  10. todo: state.getIn(['todo', 'todo'])
  11. });
  12. const mapDispatchToProps = (dispatch) => ({
  13. // 當使用者在 input 輸入資料值即會觸發這個函數,發出 changeText action 並附上使用者輸入內容 event.target.value
  14. onChangeText: (event) => (
  15. dispatch(changeText({ text: event.target.value }))
  16. ),
  17. // 當使用者按下送出時,發出 createTodo action 並清空 input
  18. onCreateTodo: () => {
  19. dispatch(createTodo());
  20. dispatch(changeText({ text: '' }));
  21. }
  22. });
  23. export default connect(
  24. mapStateToProps,
  25. mapDispatchToProps,
  26. )(TodoHeader);

以下是 src/containers/TodoListContainer/TodoListContainer.js 的部份:

  1. import { connect } from 'react-redux';
  2. import TodoList from '../../components/TodoList';
  3. import {
  4. deleteTodo,
  5. } from '../../actions';
  6. const mapStateToProps = (state) => ({
  7. todos: state.getIn(['todo', 'todos'])
  8. });
  9. const mapDispatchToProps = (dispatch) => ({
  10. onDeleteTodo: (index) => () => (
  11. dispatch(deleteTodo({ index }))
  12. )
  13. });
  14. export default connect(
  15. mapStateToProps,
  16. mapDispatchToProps,
  17. )(TodoList);

Presentational Components

以下是 src/components/TodoHeader/TodoHeader.js 的部份:

  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. // 開始建設 Component 並使用 connect 進來的 props 並綁定事件(onChange、onClick)。注意我們的 state 因為是使用 `ImmutableJS` 所以要用 `get()` 取值
  4. const TodoHeader = ({
  5. onChangeText,
  6. onCreateTodo,
  7. todo,
  8. }) => (
  9. <div>
  10. <h1>TodoHeader</h1>
  11. <input type="text" value={todo.get('text')} onChange={onChangeText} />
  12. <button onClick={onCreateTodo}>送出</button>
  13. </div>
  14. );
  15. export default TodoHeader;

以下是 src/components/TodoList/TodoList.js 的部份:

  1. import React from 'react';
  2. import ReactDOM from 'react-dom';
  3. // Component 部分值的注意的是 todos state 是透過 map function 去迭代出元素,由於要讓 React JSX 可以渲染並保持傳入觸發 event state 的 immutable,所以需使用 toJS() 轉換 component of array。
  4. // 由 Component 傳進欲刪除元素的 index
  5. const TodoList = ({
  6. todos,
  7. onDeleteTodo,
  8. }) => (
  9. <div>
  10. <ul>
  11. {
  12. todos.map((todo, index) => (
  13. <li key={index}>
  14. {todo.get('text')}
  15. <button onClick={onDeleteTodo(index)}>X</button>
  16. </li>
  17. )).toJS()
  18. }
  19. </ul>
  20. </div>
  21. );
  22. export default TodoList;

總結

That’s it!透過區分 Container 與 Presentational Components 可以讓程式架構和職責更清楚了!接下來我們將運用我們所學實際開發兩個貼近生活的專案,讓讀者更加熟悉 React 生態系如何應用於實務上。

延伸閱讀

  1. @dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.vtcuxsurv">Presentational and Container Components
  2. Redux Usage with React
  3. @franleplant/react-higher-order-components-in-depth-cf9032ee6c3e#.r8srulpaj">React Higher Order Components in depth
  4. React higher order components

| 勘誤、提問或許願 |