Redux 包教包會(二):趁熱打鐵,重拾初心

在這一部分中,咱們將趁熱打鐵,運用上篇教程學到的 Redux 三大核心概念來將待辦事項的剩下部分重構完成,它涉及到將 TodoList 和 Footer 部分的相關代碼重構到 Redux,並使用 Redux combineReducers API 進行邏輯拆分和組合,使得咱們能夠在使用 Redux 便利的同時,又不至於讓應用的邏輯看起來臃腫不堪,複用 React 組件化的便利,咱們可讓狀態的處理也 「組件化」。最後,咱們將讓 React 迴歸初心——專一於展示用戶界面,經過「容器組件」和「展現組件」將邏輯和狀態進一步分離。前端

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

此教程屬於React 前端工程師學習路線的一部分,歡迎來 Star 一波,鼓勵咱們繼續創做出更好的教程,持續更新中~。react

重構代碼:將 TodoList 部分遷移到 Redux

上一篇教程中,咱們已經把 Redux 的核心概念講完了,而且運用這些概念重構了一部分待辦事項應用,在這一小節中,咱們將完整地運用以前學到的知識,繼續用 Redux 重構咱們的應用。若是你沒有讀過上篇教程,想直接從這一步開始,那麼請運行如下命令:git

git clone https://github.com/pftom/redux-quickstart-tutorial.git
cd redux-quickstart-tutorial
git checkout second-part
npm install && npm start
複製代碼

此時若是你在瀏覽器裏面嘗試查看這個待辦事項小應用,你會發現它還只能夠添加新的待辦事項,對於 「完成和重作待辦事項」 以及 「過濾查看待辦事項」 這兩個功能,目前咱們尚未使用 Redux 實現。因此當你點擊單個待辦事項時,瀏覽器會報錯;當你點擊底部的三個過濾器按鈕時,瀏覽器不會有任何反應。github

在這一小節中,咱們將使用 Redux 重構 「完成和重作待辦事項」 功能,即你能夠經過點擊某個待辦事項來完成它。算法

咱們將運用 Redux 最佳實踐的開發方式來重構這一功能:npm

  • 定義 Action Creators
  • 定義 Reducers
  • connect 組件以及在組件中 dispatch Action

之後在開發 Redux 應用的時候,均可以使用這三步流程來周而復始地開發新的功能,或改進現有的功能。編程

定義 Action Creators

首先咱們要定義 「完成待辦事項」 這一功能所涉及的 Action,打開 src/actions/index.js,在最後面添加 toggleTodoredux

// 省略 nextTodoId 和 addTodo ...

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

能夠看到,咱們定義並導出了一個 toggleTodo 箭頭函數,它接收 id 並返回一個類型爲 "TOGGLE_TODO" 的 Action。設計模式

定義 Reducers

接着咱們來定義響應 dispatch(action) 的 Reducers,打開 src/index.js,修改 rootReducer 函數以下:

// ...

const rootReducer = (state, action) => {
  switch (action.type) {
    case "ADD_TODO": {
      const { todos } = state;

      return {
        ...state,
        todos: [
          ...todos,
          {
            id: action.id,
            text: action.text,
            completed: false
          }
        ]
      };
    }

    case "TOGGLE_TODO": {
      const { todos } = state;

      return {
        ...state,
        todos: todos.map(todo =>
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
        )
      };
    }
    default:
      return state;
  }
};

// ...
複製代碼

能夠看到,咱們在 switch 語句裏面添加了一個 "TOGGLE_TODO" 的判斷,並根據 action.id 來判斷對應操做的 todo,取反它目前的 completed 屬性,用來表示從完成到未完成,或從未完成到完成的操做。

connect 和 dispatch(action)

當定義了 Action,聲明瞭響應 Action 的 Reducers 以後,咱們開始定義 React 和 Redux 交流的接口:connectdispatch,前者負責將 Redux Store 的內容整合進 React,後者負責從 React 中發出操做 Redux Store 的指令。

咱們打開 src/components/TodoList.js 文件,對文件內容做出以下的修改:

// 省略沒有變化的 import 語句 ...

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

const TodoList = ({ todos, dispatch }) => (
  <ul> {todos.map(todo => ( <Todo key={todo.id} {...todo} onClick={() => dispatch(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 }; export default connect()(TodoList); 複製代碼

能夠看到,咱們對文件作出瞭如下幾步修改:

  • 首先從 react-redux 中導出 connect 函數,它負責給 TodoList 傳入 dispatch 函數,使得咱們能夠在 TodoList 組件中 dispatch Action。
  • 而後咱們導出了 toggleTodo Action Creators,並將以前從父組件接收 toggleTodo 方法並調用的方式改爲了當 Todo 被點擊以後,咱們 dispatch(toggle(todo.id))
  • 咱們刪除 propsTypes 中再也不須要的 toggleTodo

刪除無用代碼

當咱們經過以上三步整合了 Redux 的內容以後,咱們就能夠刪除原 App.js 中沒必要要的代碼了,修改後的 src/components/App.js 內容以下:

// ...

class App extends React.Component {
  constructor(props) {
    super(props);

    this.setVisibilityFilter = this.setVisibilityFilter.bind(this);
  }

  setVisibilityFilter(filter) {
    this.setState({
      filter: filter
    });
  }

  render() {
    const { todos, filter } = this.props;

    return (
      <div>
        <AddTodo />
        <TodoList todos={getVisibleTodos(todos, filter)} />
        <Footer
          filter={filter}
          setVisibilityFilter={this.setVisibilityFilter}
        />
      </div>
    );
  }
}

// ...
複製代碼

能夠看到,咱們刪除了 toggleTodo 方法,並對應刪除了定義在 constructor 中的 toggleTodo 定義以及在 render 方法中,傳給 TodoListtoggleTodo 屬性。

保存上述修改的代碼,打開瀏覽器,你應該又能夠點擊單個待辦事項來完成和重作它了:

小結

在本節中,咱們介紹了開發 Redux 應用的最佳實踐,並經過重構 "完成和重作待辦事項「 這一功能來詳細實踐了這一最佳實踐。

重構代碼:將 Footer 部分遷移到 Redux

這一節中,咱們將繼續重構剩下的部分。咱們將繼續遵循上一節提到的 Redux 開發的最佳實踐:

  • 定義 Action Creators
  • 定義 Reducers
  • connect 組件以及在組件中 dispatch Action

定義 Action Creators

打開 src/actions/index.js 文件,在最後面添加 setVisibilityFilter

// ...

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

能夠看到咱們建立了一個名爲 setVisibilityFilter 的 Action Creators,它接收 filter 參數,而後返回一個類型爲 "SET_VISIBILITY_FILTER" 的 Action。

定義 Reducers

打開 src/index.js 文件,修改 rootReducer 以下:

// ...

const rootReducer = (state, action) => {
  switch (action.type) {
    // 省略處理 ADD_TODO 和 TOGGLE_TODO 的 reducers ...

    case "SET_VISIBILITY_FILTER": {
      return {
        ...state,
        filter: action.filter
      };
    }

    default:
      return state;
  }
};

// ...
複製代碼

能夠看到,咱們增長了一條 case 語句,來響應 "SET_VISIBILITY_FILTER" Action,經過接收新的 filter 來更新 Store 中的狀態。

connect 和 dispatch(action)

打開 src/components/Footer.js 文件,修改代碼以下:

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

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);
複製代碼

能夠看到,上面的文件主要作了這幾件事:

  • 首先從 react-redux 中導出 connect 函數,它負責給 Footer 傳入 dispatch 函數,使得咱們能夠在 Footer 組件中 dispatch Action。
  • 而後咱們導出了 setVisibilityFilter Action Creators,並將以前從父組件接收 setVisibilityFilter 方法並調用的方式改爲了當 Link 被點擊以後,咱們 dispatch 對應的 Action 。

刪除無用代碼

當咱們經過以上三步整合了 Redux 的內容以後,咱們就能夠刪除原 App.js 中沒必要要的代碼了,打開 src/components/App.js 修改內容以下:

// ...

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

    return (
      <div>
        <AddTodo />
        <TodoList todos={getVisibleTodos(todos, filter)} />
        <Footer filter={filter} />
      </div>
    );
  }
}

// ...
複製代碼

能夠看到,咱們刪除了 setVisibilityFilter 方法,並對應刪除了定義在 constructor 中的 setVisibilityFilter 定義以及在 render 方法中,傳給 FootersetVisibilityFilter 屬性。

由於 constructor 方法中已經不須要再定義內容了,因此咱們刪掉了它。

保存上述修改的代碼,打開瀏覽器,你應該又能夠繼續點擊底部的按鈕來過濾完成和未完成的待辦事項了:

小結

在本節中,咱們介紹了開發 Redux 應用的最佳實踐,並經過重構 "過濾查看待辦事項「 這一功能來詳細實踐了這一最佳實踐。

自此,咱們已經使用 Redux 重構了整個待辦事項小應用,可是重構完的這份代碼還顯得有點亂,不一樣類型的組件狀態混在一塊兒。當咱們的應用逐漸變得複雜時,咱們的 rootReducer 就會變得很是冗長,因此是時候考慮拆分不一樣組件的狀態了。

咱們將在下一節中講解如何將不一樣組件的狀態進行拆分,以確保咱們在編寫大型應用時也能夠顯得很從容。

combineReducers:組合拆分狀態的 Reducers

當應用邏輯逐漸複雜的時候,咱們就要考慮將巨大的 Reducer 函數拆分紅一個個獨立的單元,這在算法中被稱爲 」分而治之「。

Reducers 在 Redux 中其實是用來處理 Store 中存儲的 State 中的某個部分,一個 Reducer 和 State 對象樹中的某個屬性一一對應,一個 Reducer 負責處理 State 中對應的那個屬性。好比咱們來看一下如今咱們的 State 的結構:

const initialState = {
  todos: [
    {
      id: 1,
      text: "你好, 圖雀",
      completed: false
    },
    {
      id: 2,
      text: "我是一隻小小小小圖雀",
      completed: false
    },
    {
      id: 3,
      text: "小若燕雀,亦可一展宏圖!",
      completed: false
    }
  ],
  filter: VisibilityFilters.SHOW_ALL
};
複製代碼

由於 Reducer 對應着 State 相關的部分,這裏咱們的 State 有兩個部分:todosfilter,因此咱們能夠編寫兩個對應的 Reducer。

編寫 Reducer:todos

在 Redux 最佳實踐中,由於 Reducer 對應修改 State 中的相關部分,當 State 對象樹很大時,咱們的 Reducer 也會有不少,因此咱們通常會單獨建一個 reducers 文件夾來存放這些 "reducers「。

咱們在 src 目錄下新建 reducers 文件夾,而後在裏面新建一個 todos.js 文件,表示處理 State 中對應 todos 屬性的 Reducer,代碼以下:

const initialTodoState = [
  {
    id: 1,
    text: "你好, 圖雀",
    completed: false
  },
  {
    id: 2,
    text: "我是一隻小小小小圖雀",
    completed: false
  },
  {
    id: 3,
    text: "小若燕雀,亦可一展宏圖!",
    completed: false
  }
];

const todos = (state = initialTodoState, 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;
複製代碼

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

  • 首先咱們將原 initialState 裏面的 todos 部分拆分到了 src/reducers/todos.js 文件裏,咱們定義了一個 initialTodoState 表明以前的 initialStatetodos 部分,它是一個數組,並把它賦值給 todos 函數中 state 參數的默認值,即當調用此函數時,若是傳進來的 state 參數爲 undefined 或者 null 時,這個 state 就是 initialState
  • 接着咱們定義了一個 todos 箭頭函數,它的結構和 rootReducer 相似,都是接收兩個參數:stateaction,而後進入一個 switch 判斷語句,根據 action.type 判斷要相應的 Action 類型,而後對 state 執行對應的操做。

注意

咱們的 todos reducers 只負責處理原 initialStatetodos 部分,因此這裏它的 state 就是原 todos 屬性,它是一個數組,因此咱們在 switch 語句裏,進行數據改變時,要對數組進行操做,並最後返回一個新的數組。

編寫 Reducer:filter

咱們前面使用 todos reducer 解決了原 initialStatetodos 屬性操做問題,如今咱們立刻來說解剩下的 filter 屬性的操做問題。

src/reducers 文件夾下建立 filter.js 文件,在其中加入以下的內容:

import { VisibilityFilters } from "../components/App";

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

export default filter;
複製代碼

能夠看到咱們定義了一個 filter 箭頭函數,它接收兩個參數:stateaction,由於這個 filter reducer 只負責處理原 initialStatefilter 屬性部分,因此這裏這個 state 參數就是原 filter 屬性,這裏咱們給了它一個默認值。

注意

filter 函數的剩餘部分和 rootReducer 相似,可是注意這裏它的 state 是對 filter 屬性進行操做,因此當判斷 "SET_VISIBILITY_FILTER" action 類型時,它只是單純的返回 action.filter

組合多個 Reducer

當咱們將 rootReducer 的邏輯拆分,並對應處理 Store 中保存的 State 中的屬性以後,咱們能夠確保每一個 reducer 都很小,這個時候咱們就要考慮如何將這些小的 reducer 組合起來,構成最終的 rootReducer,這種組合就像咱們組合 React 組件同樣,最終只有一個根級組件,在咱們的待辦事項小應用裏面,這個組件就是 App.js 組件。

Redux 爲咱們提供了 combineReducers API,用來組合多個小的 reducer,咱們在 src/reducers 文件夾下建立 index.js 文件,並在裏面添加以下內容:

import { combineReducers } from "redux";

import todos from "./todos";
import filter from "./filter";

export default combineReducers({
  todos,
  filter
});
複製代碼

能夠看到,咱們從 redux 模塊中導出了 combineReducers 函數,而後導出了以前定義的 todosfilter reducer。

接着咱們經過對象簡潔表示法,將 todosfilter 做爲對象屬性合在一塊兒,而後傳遞給 combineReducers 函數,這裏 combineReducers 內部就會對 todosfilter 進行操做,而後生成相似咱們以前的 rootReducer 形式。最後咱們導出生成的 rootReducer

combineReducers 主要有兩個做用:

1)組合全部 reducer 的 state,最後組合成相似咱們以前定義的 initialState 對象狀態樹。

即這裏 todos reducer 的 state 爲:

state = [
  {
    id: 1,
    text: "你好, 圖雀",
    completed: false
  },
  {
    id: 2,
    text: "我是一隻小小小小圖雀",
    completed: false
  },
  {
    id: 3,
    text: "小若燕雀,亦可一展宏圖!",
    completed: false
  }
];
複製代碼

filter reducer 的 state 爲:

state = VisibilityFilters.SHOW_ALL
複製代碼

那麼經過 combineReducers 組合這兩個 reducerstate 獲得的最終結果爲:

state = {
  todos: [
    {
      id: 1,
      text: "你好, 圖雀",
      completed: false
    },
    {
      id: 2,
      text: "我是一隻小小小小圖雀",
      completed: false
    },
    {
      id: 3,
      text: "小若燕雀,亦可一展宏圖!",
      completed: false
    }
  ],
  filter: VisibilityFilters.SHOW_ALL
};
複製代碼

這個經過 combineReducers 組合後的最終 state 就是存儲在 Store 裏面的那棵 State JavaScript 對象狀態樹。

2)分發 dispatch 的 Action。

經過 combineReducers 組合 todosfilter reducer 以後,從 React 組件中 dispatch Action會遍歷檢查 todosfilter reducer,判斷是否存在響應對應 action.typecase 語句,若是存在,全部的這些 case 語句都會響應。

刪除沒必要要的代碼

當咱們將原 rootReducer 拆分紅了 todosfilter 兩個 reducer ,並經過 redux 提供的 combineReducers API 進行組合後,咱們以前在 src/index.js 定義的 initialStaterootReducer 就再也不須要了,刪除後整個文件的代碼以下:

import React from "react";
import ReactDOM from "react-dom";
import App, { VisibilityFilters } from "./components/App";

import { createStore } from "redux";
import { Provider } from "react-redux";
import rootReducer from "./reducers";

const store = createStore(rootReducer);

ReactDOM.render(
  <Provider store={store}> <App /> </Provider>,
  document.getElementById("root")
);
複製代碼

能夠看到,咱們從刪除了以前在 src/index.js 定義的 rootReducer,轉而使用了從 src/reducers/index.js 導出的 rootReducer

而且咱們咱們以前講到,combineReducers 的第一個功能就是組合多個 reducer 的 state,最終合併成一個大的 JavaScript 對象狀態樹,而後自動存儲在 Redux Store 裏面,因此咱們再也不須要給 createStore 顯式的傳遞第二個 initialState 參數了。

保存修改的內容,打開瀏覽器,能夠照樣能夠操做全部的功能,你能夠加點待辦事項,點擊某個待辦事項以完成它,經過底部的三個過濾按鈕查看不一樣狀態下的待辦事項:

小結

在這一小節中,咱們講解了 redux 提供的 combineReducers API,它主要解決兩個問題:

  • 當應用逐漸複雜的時候,咱們須要對 Reducer 進行拆分,那麼咱們就須要把拆分後的 Reducer 進行組合,併合並全部的 State。
  • 對於每一個 React 組件 dispatch 的 Action,將其分發給對應的 Reducer。

當有了 combineReducers 以後,無論咱們的應用如何複雜,咱們均可以將處理應用狀態的邏輯拆分都一個一個很簡潔、易懂的小文件,而後組合這些小文件來完成複雜的應用邏輯,這和 React 組件的組合思想相似,能夠想見,組件式編程的威力是多麼巨大!

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

展現組件和容器組件

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

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

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

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

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

// 省略了 nextTodoId 和以前定義的三個 Action

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 修改 VisibilityFilters 的導包路徑:

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

// ...
複製代碼

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

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

// ...
複製代碼

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

const todos = (state = [], action) => {
  switch (action.type) {
    // ...
  }
};

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 也被冠名爲 」可預測的狀態管理容器「。

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

相關文章
相關標籤/搜索