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

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

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

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

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

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

此時如果你在瀏覽器裡面嘗試查看這個待辦事項小應用,你會發現它還只可以添加新的待辦事項,對於 「完成和重做待辦事項」 以及 「過濾查看待辦事項」 這兩個功能,目前我們還沒有使用 Redux 實現。所以當你點擊單個待辦事項時,瀏覽器會報錯;當你點擊底部的三個過濾器按鈕時,瀏覽器不會有任何反應。

在這一小節中,我們將使用 Redux 重構 「完成和重做待辦事項」 功能,即你可以通過點擊某個待辦事項來完成它。

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

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

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

定義 Action Creators

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

// 省略 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 應用的最佳實踐,並通過重構 "完成和重做待辦事項「 這一功能來詳細實踐了這一最佳實踐。

這一節中,我們將繼續重構剩下的部分。我們將繼續遵循上一節提到的 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 組件的狀態和渲染抽離。

編寫容器組件

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

References

[1] React 前端工程師學習路線: https://github.com/tuture-dev/react-roadmap