Redux 包教包會(三):使用容器組件和展現組件近一步分離組件狀態

在這一部分中,咱們會提出 「容器組件」 和 「展現組件」 的概念,「容器組件」 用於接管 「狀態」,「展現組件」 用於渲染界面,其中 「展現組件」 也是 React 誕生的初心,專一於高效的編寫用戶界面。前端

若是您以爲咱們寫得還不錯,記得 點贊 + 關注 + 評論 三連,鼓勵咱們寫出更好的教程💪

重構代碼:將 TodoList 的狀態和渲染分離

歡迎閱讀 Redux 包教包會系列:node

此教程屬於 React 前端工程師學習路線的一部分,點擊可查看所有內容。

展現組件和容器組件

Redux 的出現,經過將 State 從 React 組件剝離,並將其保存在 Store 裏面,來確保狀態來源的可預測性,你可能以爲這樣就已經很好了,可是 Redux 的動做還沒完,它又進一步提出了展現組件(Presentational Components)和容器組件(Container Components)的概念,將純展現性的 React 組件和狀態進一步抽離。react

當咱們把 Redux 狀態循環圖中的 View 層進一步拆分時,它看起來是這樣的:git

即咱們在最終渲染界面的組件和 Store 中存儲的 State 之間又加了一層,咱們稱這一層爲它專門負責接收來自 Store 的 State,並把組件中想要發起的狀態改變組裝成 Action,而後經過 dispatch 函數發出。github

將狀態完全剝離以後剩下的那層稱之爲展現組件,它專門接收來自容器組件的數據,而後將其渲染成 UI 界面,並在須要改變狀態時,告知容器組件,讓其代爲 dispatch Action。redux

首先,咱們將 App.js 中的 VisibilityFilters 移動到了 src/actions/index.js 中。由於 VisibilityFilters 定義了過濾展現 TodoList 的三種操做,和 Action 的含義更相近一點,因此咱們將類似的東西放在了一塊兒。修改 src/actions/index.js 以下:segmentfault

let nextTodoId = 0;

export const addTodo = text => ({
  type: "ADD_TODO",
  id: nextTodoId++,
  text
});

export const toggleTodo = id => ({
  type: "TOGGLE_TODO",
  id
});

export const setVisibilityFilter = filter => ({
  type: "SET_VISIBILITY_FILTER",
  filter
});

export const VisibilityFilters = {
  SHOW_ALL: "SHOW_ALL",
  SHOW_COMPLETED: "SHOW_COMPLETED",
  SHOW_ACTIVE: "SHOW_ACTIVE"
};

編寫容器組件

容器組件其實也是一個 React 組件,它只是將原來從 Store 到 View 的狀態和從組件中 dispatch Action 這兩個邏輯從原組件中抽離出來。設計模式

根據 Redux 的最佳實踐,容器組件通常保存在 containers 文件夾中,咱們在 src 文件夾下創建一個 containers 文件夾,而後在裏面新建 VisibleTodoList.js 文件,用來表示原 TodoList.js 的容器組件,並在文件中加入以下代碼:前端工程師

import { connect } from "react-redux";
import { toggleTodo } from "../actions";
import TodoList from "../components/TodoList";
import { VisibilityFilters } from "../actions";

const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case VisibilityFilters.SHOW_ALL:
      return todos;
    case VisibilityFilters.SHOW_COMPLETED:
      return todos.filter(t => t.completed);
    case VisibilityFilters.SHOW_ACTIVE:
      return todos.filter(t => !t.completed);
    default:
      throw new Error("Unknown filter: " + filter);
  }
};

const mapStateToProps = state => ({
  todos: getVisibleTodos(state.todos, state.filter)
});

const mapDispatchToProps = dispatch => ({
  toggleTodo: id => dispatch(toggleTodo(id))
});

export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

能夠看到,上面的代碼主要作了這幾件事情:函數

  • 咱們定義了一個 mapStateToProps ,這是咱們以前詳細講解過,它主要是能夠獲取到來自 Redux Store 的 State 以及組件自身的原 Props,而後組合這二者成新的 Props,而後傳給組件,這個函數是 Store 到組件的惟一接口。這裏咱們將以前定義在 App.js 中的 getVisibleTodos 函數移過來,並根據 state.filter 過濾條件返回相應須要展現的 todos
  • 接着咱們定義了一個沒見過的 mapDispatchToProps 函數,這個函數接收兩個參數:dispatchownProps,前者咱們很熟悉了就是用來發出更新動做的函數,後者就是原組件的 Props,它是一個可選參數,這裏咱們沒有聲明它。咱們主要在這個函數聲明式的定義全部須要 dispatch 的 Action 函數,並將其做爲 Props 傳給組件。這裏咱們定義了一個 toggleTodo 函數,使得在組件中經過調用 toggleTodo(id) 就能夠 dispatch(toggleTodo(id))
  • 最後咱們經過熟悉的 connect 函數接收 mapStateToPropsmapDispatchToProps並調用,而後再接收 TodoList 組件並調用,返回最終的導出的容器組件。

編寫展現組件

當咱們編寫了 TodoList 的容器組件以後,接着咱們要考慮就是抽離了 State 和 dispatch 的關於 TodoList 的展現組件了。

打開 src/components/TodoList.js 對文件作出相應的改動以下:

import React from "react";
import PropTypes from "prop-types";
import Todo from "./Todo";

const TodoList = ({ todos, toggleTodo }) => (
  <ul>
    {todos.map(todo => (
      <Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} />
    ))}
  </ul>
);

TodoList.propTypes = {
  todos: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number.isRequired,
      completed: PropTypes.bool.isRequired,
      text: PropTypes.string.isRequired
    }).isRequired
  ).isRequired,
  toggleTodo: PropTypes.func.isRequired
};

export default TodoList;

在上面的代碼中,咱們刪除了 connecttoggleTodo Action,並將 TodoList 接收的 dispatch 屬性刪除,轉而改爲經過 mapDispatchToProps 傳進來的 toggleTodo 函數,並在 Todo 被點擊時調用 toggleTodo 函數。

固然咱們的 toggleTodo 屬性又回來了,因此咱們在 propTypes 中恢復以前刪除的 toggleTodo 。:)

最後,咱們再也不須要 connect()(TodoList),由於 VisibleTodoList.js 中定義的 TodoList 的對應容器組件會取到 Redux Store 中的 State,而後傳給 TodoList。

能夠看到,TodoList 不用再考慮狀態相關的操做,只須要專心地作好界面的展現和動做的響應。咱們進一步將狀態與渲染分離,讓合適的人作 TA 最擅長的事。

一些瑣碎的收尾工做

由於咱們將原來的 TodoList 剝離成了容器組件和 展現組件,因此咱們要將 App.js 裏面對應的 TodoList 換成咱們的 VisibleTodoList,由容器組件來提供原 TodoList 對外的接口。

咱們打開 src/components/App.js 對相應的內容做出以下修改:

import React from "react";
import AddTodo from "./AddTodo";
import VisibleTodoList from "../containers/VisibleTodoList";
import Footer from "./Footer";

import { connect } from "react-redux";

class App extends React.Component {
  render() {
    const { filter } = this.props;

    return (
      <div>
        <AddTodo />
        <VisibleTodoList />
        <Footer filter={filter} />
      </div>
    );
  }
}

const mapStateToProps = (state, props) => ({
  filter: state.filter
});

export default connect(mapStateToProps)(App);

能夠看到咱們作了這麼幾件事:

  • 將以前的 TodoList 更換成 VisibleTodoList。
  • 刪除 VisibilityFilters,由於它已經被放到了 src/actions/index.js
  • 刪除 getVisibleTodos,由於它已經被放到了 VisibleTodoList 中。
  • 刪除 mapStateToProps 中獲取 todos 的操做,由於咱們已經在 VisibleTodoList 中獲取了。
  • 刪除對應在 App 組件中的 todos

接着咱們處理一下因 VisibilityFilters 變更而引發的其餘幾個文件的導包問題。

打開 src/components/Footer.js 修改導包路徑:

import React from "react";
import Link from "./Link";
import { VisibilityFilters } from "../actions";

import { connect } from "react-redux";
import { setVisibilityFilter } from "../actions";

const Footer = ({ filter, dispatch }) => (
  <div>
    <span>Show: </span>
    <Link
      active={VisibilityFilters.SHOW_ALL === filter}
      onClick={() => dispatch(setVisibilityFilter(VisibilityFilters.SHOW_ALL))}
    >
      All
    </Link>
    <Link
      active={VisibilityFilters.SHOW_ACTIVE === filter}
      onClick={() =>
        dispatch(setVisibilityFilter(VisibilityFilters.SHOW_ACTIVE))
      }
    >
      Active
    </Link>
    <Link
      active={VisibilityFilters.SHOW_COMPLETED === filter}
      onClick={() =>
        dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))
      }
    >
      Completed
    </Link>
  </div>
);

export default connect()(Footer);

打開 src/reducers/filter.js 修改導包路徑:

import { VisibilityFilters } from "../actions";

const filter = (state = VisibilityFilters.SHOW_ALL, action) => {
  switch (action.type) {
    case "SET_VISIBILITY_FILTER":
      return action.filter;
    default:
      return state;
  }
};

export default filter;

由於咱們在 src/actions/index.js 中的 nextTodoId 是從 0 開始自增的,因此以前咱們定義的 initialTodoState 會出現一些問題,好比新添加的 todo 的 id 會與初始的重疊,致使出現問題,因此咱們刪除 src/reducers/todos.js 中對應的 initialTodoState,而後給 todos reducer 的 state 賦予一個 [] 的默認值。

const todos = (state = [], action) => {
  switch (action.type) {
    case "ADD_TODO": {
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ];
    }

    case "TOGGLE_TODO": {
      return state.map(todo =>
        todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      );
    }

    default:
      return state;
  }
};

export default todos;

小結

保存修改的內容,你會發現咱們的待辦事項小應用依然能夠完整的運行,可是咱們已經成功的將原來的 TodoList 分離成了容器組件的 VisibleTodoList 以及展現組件的 TodoList 了。

重構代碼:將 Footer 的狀態和渲染分離

咱們趁熱打鐵,用上一節學到的知識來立刻將 Footer 組件的狀態和渲染抽離。

編寫容器組件

咱們在 src/containers 文件夾下建立一個 FilterLink.js 文件,添加對應的內容以下:

import { connect } from "react-redux";
import { setVisibilityFilter } from "../actions";
import Link from "../components/Link";

const mapStateToProps = (state, ownProps) => ({
  active: ownProps.filter === state.filter
});

const mapDispatchToProps = (dispatch, ownProps) => ({
  onClick: () => dispatch(setVisibilityFilter(ownProps.filter))
});

export default connect(mapStateToProps, mapDispatchToProps)(Link);

能夠看到咱們作了如下幾件工做:

  • 定義 mapStateToProps,它負責比較 Redux Store 中保存的 State 的 state.filter 屬性和組件接收父級傳下來的 ownProps.filter 屬性是否相同,若是相同,則把 active 設置爲 true
  • 定義 mapDispatchToProps,它經過返回一個 onClick 函數,當組件點擊時,調用生成一個 dispatch Action,將此時組件接收父級傳下來的 ownProps.filter 參數傳進 setVisibilityFilter ,生成 action.type"SET_VISIBILITY_FILTER" 的 Action,並 dispatch 這個 Action。
  • 最後咱們經過 connect 組合這二者,將對應的屬性合併進 Link 組件並導出。咱們如今應該能夠在 Link 組件中取到咱們在上面兩個函數中定義的 activeonClick 屬性了。

編寫展現組件

接着咱們來編寫原 Footer 的展現組件部分,打開 src/components/Footer.js 文件,對相應的內容做出以下的修改:

import React from "react";
import FilterLink from "../containers/FilterLink";
import { VisibilityFilters } from "../actions";

const Footer = () => (
  <div>
    <span>Show: </span>
    <FilterLink filter={VisibilityFilters.SHOW_ALL}>All</FilterLink>
    <FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>Active</FilterLink>
    <FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>Completed</FilterLink>
  </div>
);

export default Footer;

能夠看到上面的代碼修改作了這麼幾件工做:

  • 咱們將以前的導出 Link 換成了 FilterLink 。請注意當組件的狀態和渲染分離以後,咱們將使用容器組件爲導出給其餘組件使用的組件。
  • 咱們使用 FilterLink 組件,並傳遞對應的三個 FilterLink 過濾器類型。
  • 接着咱們刪除再也不不須要的 connectsetVisibilityFilter 導出。
  • 最後刪除再也不須要的filterdispatch 屬性,由於它們已經在 FilterLink 中定義並傳給了 Link 組件了。

刪除沒必要要的內容

當咱們將 Footer 中的狀態和渲染拆分以後,src/components/App.js 對應的 Footer 相關的內容就再也不須要了,咱們對文件中對應的內容做出以下修改:

import React from "react";
import AddTodo from "./AddTodo";
import VisibleTodoList from "../containers/VisibleTodoList";
import Footer from "./Footer";

class App extends React.Component {
  render() {
    return (
      <div>
        <AddTodo />
        <VisibleTodoList />
        <Footer />
      </div>
    );
  }
}

export default App;

能夠看到咱們作了以下工做:

  • 刪除 App 組件中對應的 filter 屬性和 mapStateToProps 函數,由於咱們已經在 FilterLink 中獲取了對應的屬性,因此咱們再也不須要直接從 App 組件傳給 Footer 組件了。
  • 刪除對應的 connect 函數。
  • 刪除對應 connect(mapStateToProps)(),由於 App 再也不須要直接從 Redux Store 中獲取內容了。

小結

保存修改的內容,你會發現咱們的待辦事項小應用依然能夠完整的運行,可是咱們已經成功的將原來的 Footer 分離成了容器組件的 FilterLink 以及展現組件的 Footer 了。

重構代碼: 將 AddTodo 的狀態和渲染分離

讓咱們來完成最後一點收尾工做,將 AddTodo 組件的狀態和渲染分離。

編寫容器組件

咱們在 src/containers 文件夾中建立 AddTodoContainer.js 文件,在其中添加以下內容:

import { connect } from "react-redux";
import { addTodo } from "../actions";
import AddTodo from "../components/AddTodo";

const mapStateToProps = (state, ownProps) => {
  return ownProps;
};

const mapDispatchToProps = dispatch => ({
  addTodo: text => dispatch(addTodo(text))
});

export default connect(mapStateToProps, mapDispatchToProps)(AddTodo);

能夠看到咱們作了幾件熟悉的工做:

  • 定義 mapStateToProps,由於 AddTodo 不須要從 Redux Store 中取內容,因此 mapStateToProps 只是單純地填充 connect 的第一個參數,而後簡單地返回組件的原 props,不起其它做用。
  • 定義 mapDispatchToProps,咱們定義了一個 addTodo 函數,它接收 text ,而後 dispatch 一個 action.type"ADD_TODO" 的 Action。
  • 最後咱們經過 connect 組合這二者,將對應的屬性合併進 AddTodo 組件並導出。咱們如今應該能夠在 AddTodo 組件中取到咱們在上面兩個函數中定義的 addTodo 屬性了。

編寫展現組件

接着咱們來編寫 AddTodo 的展現組件部分,打開 src/components/AddTodo.js 文件,對相應的內容做出以下的修改:

import React from "react";

const AddTodo = ({ addTodo }) => {
  let input;

  return (
    <div>
      <form
        onSubmit={e => {
          e.preventDefault();
          if (!input.value.trim()) {
            return;
          }
          addTodo(input.value);
          input.value = "";
        }}
      >
        <input ref={node => (input = node)} />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  );
};

export default AddTodo;

能夠看到,上面的代碼作了這麼幾件工做:

  • 咱們刪除了導出的 connect 函數,而且去掉了其對 AddTodo 的包裹。
  • 咱們將 AddTodo 接收的屬性從 dispatch 替換成從 AddTodoContainer 傳過來的 addTodo 函數,當表單提交時,它將被調用,dispatch 一個 action.type"ADD_TODO"textinput.value 的 Action。

修改對應的內容

由於咱們將原 TodoList 分離成了容器組件 AddTodoContainer 和展現組件 TodoList,因此咱們須要對 src/components/App.js 作出以下的修改:

import React from "react";
import AddTodoContainer from "../containers/AddTodoContainer";
import VisibleTodoList from "../containers/VisibleTodoList";
import Footer from "./Footer";

class App extends React.Component {
  render() {
    return (
      <div>
        <AddTodoContainer />
        <VisibleTodoList />
        <Footer />
      </div>
    );
  }
}

export default App;

能夠看到咱們使用 AddTodoContainer 替換了原來的 AddTodo 導出,並在 render 方法中渲染 AddTodoContainer 組件。

小結

保存修改的內容,你會發現咱們的待辦事項小應用依然能夠完整的運行,可是咱們已經成功的將原來的 AddTodo 分離成了容器組件的 AddTodoContainer 以及展現組件的 AddTodo 了。

總結

到目前爲止,咱們就已經學習完了 Redux 的全部基礎概念,而且運用這些基礎概念將一個純 React 版的待辦事項一步一步重構到了 Redux。

讓咱們最後一次祭出 Redux 狀態循環圖,回顧咱們在這篇教程中學到的知識:

咱們在這篇教程中首先提出了 Redux 的三大概念:Store,Action,Reducers:

  • Store 用來保存整個應用的狀態,這個狀態是一個被稱之爲 State 的 JavaScript 對象。全部應用的狀態都是從 Store 中獲取,因此狀態的改變都是改變 Store 中的狀態,因此 Store 也有着 「數據的惟一真相來源」 的稱號。
  • Action 是 Redux 中用來改變 Store 狀態的惟一手段,全部狀態的改變都是以相似 { type: 'ACTION_TYPE', data1, data2 } 這樣的形式聲明式的定義一個 Action,而後經過 dispatch 這個 Action 來發生的。
  • Reducers 是用來響應 Action 發出的改變更做,經過 switch 語句匹配 action.type ,經過對 State 的屬性進行增刪改查,而後返回一個新 State 的操做。同時它也是一個純函數,即不會直接修改 State 自己。

具體反映到咱們重構的待辦事項項目裏,咱們使用 Store 保存的狀態來替換以前 React 中的 this.state,使用 Action 來代替以前 React 發起修改 this.state 的動做,經過 dispatch Action 來發起修改 Store 中狀態的操做,使用 Reducers 代替以前 React 中更新狀態的 this.setState 操做,純化的更新 Store 裏面保存的 State。

接着咱們趁熱打鐵,使用以前學到的三大概念,將整個待辦事情的剩下部分重構到了 Redux。

可是重構完咱們發現,咱們如今的 rootReducer 函數已經有點臃腫了,它包含了 todosfilter 兩類不一樣的狀態屬性,而且若是咱們想要繼續擴展這個待辦事項應用,那麼還會繼續添加不一樣的狀態屬性,到時候各類狀態屬性的操做夾雜在一塊兒很容易形成混亂和下降代碼的可讀性,不利於維護,所以咱們提出了 combineReducers 方法,用於切分 rootReducer 到多個分散在不一樣文件的保存着單一狀態屬性的 Reducer,,而後經過 combineReducers 來組合這些拆分的 Reducers。

詳細講解 combineReducers 的概念以後,咱們接着將以前的不徹底重構的 Redux 代碼進行了又一次重構,將 rootReducer 拆分紅了 todosfilter 兩個 Reducer。

最後咱們更進一步,讓 React 專一作好它擅長的編寫用戶界面的事情,讓應用的狀態和渲染分離,咱們提出了展現組件和容器組件的概念,前者是完徹底全的 React,接收來自後者的數據,而後負責將數據高效正確的渲染;前者負責響應用戶的操做,而後交給後者發出具體的指令,能夠看到,當咱們使用 Redux 以後,咱們在 React 上蓋了一層邏輯,這層邏輯徹底負責狀態方面的工做,這就是 Redux 的精妙之處啊!

但願看到這裏的同窗能對 Redux 有個很好的瞭解,並能靈活的結合 React 和 Redux 的使用,感謝你的閱讀!

One More Thing!

細心的讀者可能發現了,咱們畫的 Redux 狀態循環圖都是單向的,它有一個明確的箭頭指向,這其實也是 Redux 的哲學,即 」單向數據流「,也是 React 社區推崇的設計模式,再加上 Reducer 的純函數約定,這使得咱們整個應用的每一次狀態更改都是能夠被記錄下來,而且能夠重現出來,或者說狀態是可預測的,它能夠追根溯源的找到某一次狀態的改變時由某一個 Action 發起的,因此 Redux 也被冠名爲 」可預測的狀態管理容器「。

此教程屬於 React 前端工程師學習路線的一部分,點擊可查看所有內容。

想要學習更多精彩的實戰技術教程?來圖雀社區逛逛吧。

相關文章
相關標籤/搜索