­

Redux 包教包會(一):解救 React 狀態危機

前端應用的狀態管理日益複雜。隨著大前端時代的到來,前端愈來愈注重處理邏輯,而不只是專註 UI 層面的改進,而以 React 為代表的前端框架的出現,大大簡化了我們編寫 UI 介面的複雜度。雖然 React 提供了 State 機制實現狀態管理,也有諸如「狀態提升」等開發約定,但是這些方案只適用於小型應用,當你的前端應用有多達 10 個以上頁面時,如何讓應用狀態可控、讓協作開發高效成為了亟待解決的問題,而 Redux 的出現正是為了解決這些問題而生的!Redux 提出的「數據的唯一真相來源」、單向數據流、「純函數 Reducers」 大大簡化了前端邏輯,使得我們能夠以高效、便於協作的方式編寫任意複雜的前端應用。本篇教程致力於用簡短的文字講透 Redux,在實戰中掌握 Redux 的概念和精髓。

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

在我們閱讀教程之前

Redux 官方文檔對 Redux 的定義是:一個可預測的 JavaScript 應用狀態管理容器

這就意味著,Redux 是無法單獨運作的,它需要與一個具體的 View 層的前端框架相結合才能發揮出它的威力,這裡的 View 層包括但不限於 React、Vue 或者 Angular 等。這裡我們將使用 React 作為綁定視圖層,因為 Redux 最初誕生於 React 社區,為解決 React 的狀態管理問題而設計和開發的一個庫。這篇教程將讓你直觀地感受 React 的「狀態危機」,以及 Redux 是如何解決這一危機的,從而能夠更好地學習 Redux,並理解它的源起,以及它將走向什麼樣的遠方。

前提條件

本篇教程是關於 Redux 的快速入門教程,並致力於講解與 React 綁定時的使用,而了解和掌握 Redux 對於一個 React 開發者來說屬於較為進階的內容,所以我們假設在閱讀本篇教程之前,你需要擁有以下的知識儲備:

•對 ES6 的函數、類、const、對象解構、函數默認參數等概念有良好的了解,當然如果你了解過函數式編程,如純函數、不變性等就更好了•對 React 有良好的了解,當然如果有獨立開發過至少有 5 個頁面的 React 應用的經驗就更好了,可以參考這篇入門教程[2]進行學習•了解 Node 和 npm,有過相關的安裝依賴的經驗即可,可以參考這篇教程[3]進行學習

你將學到什麼

在本篇教程中,我們將首先給出了一個使用 React 實現的待辦事項小應用[4](比上篇教程[5]中完成的版本多了篩選的功能),它將是我們學習 Redux 的起點,當你熟悉了這份初始程式碼,並了解了它的功能之後,你就可以關閉它,然後開始我們教程的學習啦!

我們將基於這個純 React 寫成的模板,分析 React 在處理狀態時存在的問題,以及用 Redux 重構帶來的優勢。接著我們將通過實戰的方式學習如何將一個純 React 應用一步步地重構成一個 Redux 應用,最終實現一個升級版的待辦事項小應用。

程式碼和最終效果

本教程所實現的源程式碼都託管在 Github 上:

•純 React 源碼:源碼地址[6]。•使用 Redux 重構後的源碼:源碼地址[7]。

你可以通過 CodeSandbox 查看程式碼最終的效果:

•純 React 效果:最終效果地址[8]。•使用 Redux 重構後的效果:最後效果地址[9]。

開始 Redux 之旅

不管外界把 Redux 吹得如何天花亂墜,實際上它可以用一張圖來概括,這張圖也有利於幫助你思考前端的本質是什麼:

我們先來詳解一下這張圖,並且在教程之後的內容中,你會多次看到這張圖以不同的形式出現。我們希望學完本篇教程之後,每當你想起 Redux 時,腦海里就是上面這張圖。

View

首先我們來看 View ,在前端開發中,我們稱這個為視圖層,就是展示給最終用戶的效果,在本篇教程的學習中,我們的 View 就是 React。

Store

隨著前端應用要完成的工作越來越豐富,我們對前端也提出了要保持 「狀態」 的要求。在 React 中,這個 「狀態」 將保存在 this.state。在 Redux 中,這個狀態將保存在 Store。

這個 Store 從抽象意義上來說可以看做一個前端的 「資料庫」,它保存著前端的狀態(state),並且分發這些狀態給 View,使得 View 根據這些狀態渲染不同的內容。

注意到,Redux 是一個可預測的 JavaScript 應用狀態管理容器,這個狀態容器就是這裡的 Store。

Reducers

我們日常生活中看到的網頁,它不是一成不變的,而是會響應用戶的 「動作」,無論是頁面跳轉還是登陸註冊,這些動作會改變當前應用的狀態。

在 Redux 框架中,Reducers 的作用就是響應不同的動作。更精確地說,Reducers 是負責更新 Store 中狀態的 JavaScript 函數

當我們對這三個核心概念有了粗略的認知之後,就可以開始 Redux 的學習了。

準備初始程式碼

將初始 React 程式碼模板 Clone 到本地,進入倉庫,並切換到 initial-code 分支(初始程式碼模板):

git clone https://github.com/pftom/redux-quickstart-tutorial.git  cd redux-quickstart-tutorial  git checkout initial-code

安裝項目依賴,並打開開發伺服器:

npm install  npm start

接著 React 開發伺服器會打開瀏覽器,如果你看到下面的效果,並且可以進行操作,那麼代表程式碼準備完成:

提示 由於我們使用 Create React App[10] 腳手架,它使用 Webpack Development Server(WDS)作為開發伺服器,因此在後面編輯程式碼的時候只需保存文件,我們的 React 應用就會自動刷新,非常方便。

探索初始程式碼

我們完成的這個待辦事項小應用比上篇教程[11]中實現的要高級一點,如下面這個動圖所示:

我們希望展示一個 todo 列表,當一個 todo 被點擊時,它將被加上刪除線表示此 todo 已經完成,我們還加上了一個輸入框,使得用戶可以增加新的 todo。在底部,我們展示了三個按鈕,可以切換展示 todo 的類型。

整份 React 程式碼組件設計如下(首先是組件,然後是組件所擁有的屬性):

TodoList 用來展示 todo 列表:

todos: Array 是一個 todo 數組,它其中的每個元素的樣子類似 { id, text, completed }。•toggleTodo(id: number) 是當一個 todo 被點擊時會調用的回調函數。

Todo 是單一 todo 組件:

text: string 是這個 todo 將顯示的內容。•completed: boolean 用來表示是否完成,如果完成,那麼樣式上就會給這個元素划上刪除線。•onClick() 是當這個 todo 被點擊時將調用的回調函數。

Link 是一個展示過濾的按鈕:

active: boolean 代表此時被選中,那麼此按鈕將不能被點擊•onClick() 表示這個 link 被點擊時將調用的回調函數。•children: ReactComponent 展示子組件

Footer 用於展示三個過濾按鈕:

filter: string 代表此時的被選中的過濾器字元串,它是 [SHOW_ALL, SHOW_COMPLETED, SHOW_ACTIVE] 其中之一。•setVisibilityFilter() 代表 Link 被點擊時將設置對應被點擊的 filter 的回調函數。

App 是 React 根組件,最終組合其他組件並使用 ReactDOM 對其進行編譯渲染,我們在它的 state 上定義了上面的幾個組件會用到的屬性,同時定義了其他組件會用到的方法,還有 nextTodoIdVisibilityFiltersgetVisibleTodos 等一些輔助函數。

準備 Redux 環境

我們知道 Redux 可以與多種視圖層開發框架如 React,Vue 和 Angular 等搭配使用,而 Redux 只是一個狀態管理容器,所以為了在 React 中使用 Redux,我們還需要安裝一下對應的依賴。

npm install redux  npm install react-redux

做得好!現在一切已經準備就緒,相信你已經迫不及待的想要編寫一點 Redux 相關的程式碼了,別擔心,在下一節中,我們將引出 Redux Store 的詳細概念,並且通過程式碼講解它將替換 React 的哪個部分。

理解 Store: 數據的唯一真相來源

我們前面提到了 Store 在 Redux 中的作用是用來保存狀態的,相當於我們在前端建立了一個簡單的 」資料庫「。在目前的富狀態前端應用中,如果每一次狀態的修改(例如點擊一個按鈕)都需要與後端通訊,那麼整個網站的平均響應時間將變得難以接受,用戶體驗將糟糕透頂。

根據不完全統計:」一個網站能留住一名用戶的時間只有 8S,如果你在 8S 內不能吸引住用戶,或者網站出現了問題,那麼你將徹底地丟失這名用戶!」

所以為了適應用戶的訪問需求,聰明的前端拓荒者們開始將後端的 「資料庫」 理念引入到前端中,這樣大多數的前端狀態可以直接在前端搞定,完全不需要後端的介入。

React 狀態「危機」

在 React 中,我們將狀態存在每個組件的 this.state 中,每個組件的 state 為組件所私有,如果要在一個組件中操作另外一個組件,實現起來是相當繁瑣的。

我們將用下面這張圖來演示一下為什麼繁瑣:

組件 A 是組件 B 和 C 的父組件。如果組件 B 想要操作組件 C,那麼它首先需要調用父組件 A 傳給它的 handleClick 方法,然後通過這個方法修改父組件A的 state,進而通過 React 的自動重新渲染機制,觸發組件 C 的變化。

現在組件 B 和組件 C 是處於平級的,你可能還感覺不到這種跨組件改變有什麼問題,讓我們再來看一張圖:

我們看到上面這張圖,組件 B 和組件 C 相差了很多級,圖中的 n 可能為 10,也可能更多。這個時候如果再想在組件 B 中修改組件 C,那就要把這個 handleClick 方法一層一層地往下傳。每次要修改的時候,都要進行調用,這已經相當繁瑣了。

如果組件 C 離組件 A 還有很深的層級,情況就更複雜了:

這時候,不僅要把 handleClick 方法通過很深的層級傳給組件 B,當組件 B 調用 handleClick 方法時,修改組件 A 的 state,再反過來傳遞給組件 C 時,組件 A 到組件 C 之間的所有組件都會觸發重新渲染,這帶來了巨額的渲染開銷,當我們的應用越來越複雜,這種開銷顯然是承受不起的。

解救者:Store

React 誕生的初衷就是為了更好、更高效率地編寫用戶介面 ,它不應該也不需要來承擔狀態管理的職責。

於是備受折磨的前端拓荒者們構想出了偉大的 Store。我們完全不需要讓每個組件單獨保持狀態,直接抽離所有組件的狀態,類比 React 組件樹,構造一個中心化的狀態樹,這棵狀態樹與 React 組件樹一一對應,相當於對 React 組件樹進行了狀態化建模:

可以看到,我們將組件的 state 去掉,取而代之的是一棵狀態樹,它是一個普通的 JavaScript 對象。通過對象的嵌套來類比組件的嵌套組合,這棵由 JavaScript 對象建模的狀態樹就是 Redux 中的 Store。

當我們將組件的狀態抽離出去之後,我們在使用組件 B 操作組件 C 就變得相當簡單且高效。

我們在組件 B 中發起一個更新狀態 C 的動作,此動作對應的更新函數更新 Store 狀態樹,之後將更新後的狀態 C 傳遞給組件 C,觸發組件 C 的重新渲染。

可以看到,當我們引入這種機制之後,組件 B 與組件 C 之間的交互就能夠單獨進行,不會影響 React 組件樹中的其他組件,也不需要傳遞很深層級的 handleClick 函數了,再也不需要把更新後的 state 一層一層地傳給組件 C,性能有了質的飛躍。

有了 Redux Store 之後,所有 React 應用中的狀態修改都是對這棵 JavaScript 對象樹的修改,所有狀態的獲取都是從這棵 JavaScript 對象樹獲取,這棵 JavaScript 對象代表的狀態樹成了整個應用的 「數據的唯一真相來源」。

打濕你的雙手

了解了 Redux Store 之於 React 的作用之後,我們馬上在 React 中應用 Redux ,看看神奇的 Store 是如何介入併產生如此大的變化的。

我們修改初始程式碼模板中的 src/index.js,修改後的程式碼如下:

import React from "react";  import ReactDOM from "react-dom";  import App, { VisibilityFilters } from "./components/App";    import { createStore } from "redux";  import { Provider } from "react-redux";    const initialState = {    todos: [      {        id: 1,        text: "你好, 圖雀",        completed: false      },      {        id: 2,        text: "我是一隻小小小小圖雀",        completed: false      },      {        id: 3,        text: "小若燕雀,亦可一展宏圖!",        completed: false      }    ],    filter: VisibilityFilters.SHOW_ALL  };    const rootReducer = (state, action) => {    return state;  };    const store = createStore(rootReducer, initialState);    ReactDOM.render(    <Provider store={store}>      <App />    </Provider>,    document.getElementById("root")  );

可以看到,上面的程式碼做了下面幾項工作:

•我們首先進行了導包操作,從 redux 中導出了 createStore,從 react-redux 導出了 Provider,從 src/components/App.js 中導出了 VisibilityFilters 。•接著我們定義了一個 initialState 對象,這將作為我們之後創建 Store 的初始狀態數據,也是我們之前提到的那棵 JavaScript 對象樹的初始值。•然後我們定義了一個 rootReducer 函數,它是一個箭頭函數,接收 stateaction 然後返回 state ,這個函數目前還沒有完成任何工作,但是它是創建 Store 所必須的參數之一,我們將在之後的 Reducers 中詳細講解它。•再接著,我們調用之前導出的 Redux API: createStore 函數,傳入定義的 rootReducerinitialState ,生成了我們本節的主角:store!•最後我們在 App 組件的最外層使用 Provider 包裹,並接收我們上一步創建的 store 作為參數,這確保之後我們可以在子組件中訪問到 store 中的狀態。Providerreact-redux 提供的 API,是 Redux 在 React 使用的綁定庫,它搭建起 Redux 和 React 交流的橋樑。

現在我們已經創建了 Store,並使用了 React 與 Redux 的綁定庫 react-redux 提供的 Provider 組件將 Store 與 React 組件組合在了一起。我們馬上來看一下整合 Store 與 React 之後的效果。

打開 src/components/App.js ,修改程式碼如下:

import React from "react";  import AddTodo from "./AddTodo";  import TodoList from "./TodoList";  import Footer from "./Footer";    import { connect } from "react-redux";    // 省略了 VisibilityFilters 和 getVisibleTodos 函數...    class App extends React.Component {    constructor(props) {      super(props);        this.toggleTodo = this.toggleTodo.bind(this);      this.onSubmit = this.onSubmit.bind(this);      this.setVisibilityFilter = this.setVisibilityFilter.bind(this);    }      // 省略中間其他方法...      render() {      const { todos, filter } = this.props;        return (        <div>          <AddTodo onSubmit={this.onSubmit} />          <TodoList            todos={getVisibleTodos(todos, filter)}            toggleTodo={this.toggleTodo}          />          <Footer            filter={filter}            setVisibilityFilter={this.setVisibilityFilter}          />        </div>      );    }  }    const mapStateToProps = (state, props) => ({    todos: state.todos,    filter: state.filter  });    export default connect(mapStateToProps)(App);

可以看到,上面的程式碼做了這幾項工作:

•首先我們從 react-redux 綁定庫裡面導出了 connect 函數。•然後在文件底部,我們定義了一個 mapStateToProps 箭頭函數,它接收 stateprops ,這個 state 就是我們那棵 Store 裡面保存的 JavaScript 對象狀態樹,目前就是我們在上一個文件中定義的 initialState 內容;這個 props 就是我們熟悉的原 React 組件的 props,它對於 mapStateToProps 是一個可選參數。 mapStateToProps 函數就是可以同時操作組件的原 props 和 Store 的狀態,然後合併成最終的組件 props,(當然這裡我們並沒有使用原組件 props 內容)並通過 connect 函數傳遞給 App 組件。•connect 函數接收 mapStateProps 函數,獲取 mapStateProps 返回的最終組合後的狀態,然後將其注入到 App 組件中,返回一個新的組件,然後交給 export default 導出。•經過上面的工作,我們在 App 組件中就可以取到通過 mapStateToProps 返回的 { todos, filter } 內容了,我們通過對象解構,從 this.props 拿到 todosfilter 屬性。•最後我們刪除不再需要的 constructor 中的 this.state 內容。

注意 這裡之所以我們能在 App 組件中通過 mapStateToProps 拿到 Store 中保存的 JavaScript 對象狀態樹,是因為我們在之前通過 Provider 包裹了 App 組件,並將 store 作為屬性傳遞給了 Provider

再現 Redux 環形圖

現在再來看一看我們在第一步驟中提到的環形圖,我們現在處於這個流程的第一步,即將 Store 裡面的狀態傳遞到 View 中,具體我們是通過 React 的 Redux 綁定庫 react-redux 中的 connect 實現的。

保存改變的內容,如果你的 React 開發伺服器打開著,那麼你應該可以在瀏覽器中看到如下內容:

恭喜你!你已經成功編寫了 Redux 的 Store,完成將 Redux 整合進 React 工作的 1/3。通過在 React 中接入 Store,你成功的將 Redux 和 React 之間的數據打通,並刪除了 this.state ,使用 Store 的狀態來取代 this.state

但是!當你此時點擊 Add Todo 按鈕,你的瀏覽器應該會顯示出紅色的錯誤,因為我們已經刪除了 this.state 的內容,所以在 onSubmit 方法中讀取 this.state.todos 就會報錯。別擔心,我們將在下一節中: Action 中講解如何解決這些錯誤。

理解 Action: 改變 State 的唯一手段

歡迎來到 Redux Action 環節,讓我們再一次引用上一節提到的圖:

在上一節中,我們就在組件 B 中完成某種動作來修改組件 C 中的內容,詳細剖析了完全基於 React 實現的弊端,並通過引出 Redux Store 的概念,講解了我們只需要建一個全局 JavaScript 對象狀態樹,然後所有的狀態的改變都是通過修改這一狀態樹,進而將修改後的新狀態傳給相應的組件並觸發重新渲染來完成我們的目的。並且我們講解了如何將 Store 裡面的狀態傳給 React 組件使用。

這一節我們就來講一講,如何修改 Redux Store 中保存的狀態。讓我們再拋出熟悉的 Redux 狀態環形圖:

修改 Store 中保存的狀態就是上面這張圖的第二個部分,即我們已經創建好了 Store,並在裡面存儲了一棵 JavaScript 對象狀態樹,我們通過 「發起更新動作」 來修改 Store 中保存的狀態。

Action 是什麼?

在 Redux 的概念術語中,更新 Store 的狀態有且僅有一種方式:那就是調用 dispatch 函數,傳遞一個 action 給這個函數 。

一個 Action 就是一個簡單的 JavaScript 對象:

{ type: 'ADD_TODO', text: '我是一隻小小小圖雀' }

我們可以看到一個 action 包含動作的類型,以及更新狀態需要的數據,其中 type 是必須的,其它內容都是可選的,這裡我們除了 type,還額外添加了一個 text ,代表我們發起 typeADD_TODO 的動作是,額外傳遞了一個 text 內容。

所以如果我們需要更新 Store 狀態,那麼就需要類似下面的函數調用:

dispatch({ type: 'ADD_TODO', text: '我是一隻小小小圖雀' })

使用 Action Creators

因為我們在創建 Action 的時候,有時候有些內容是固定了,比如我們的待辦事項添加教程的 Action,有三個欄位,分別是 typetextid,我們可能會要在多個地方可以 dispatch 這個 Action,那麼我們每次都需要寫下面長長的一串 :

{ type: 'ADD_TODO', text: '我是一隻小小小圖雀' , id: 0}  { type: 'ADD_TODO', text: '小若燕雀,亦可一展宏圖' , id: 1}  ...  { type: 'ADD_TODO', text: '歡迎你加入圖雀社區!' , id: 10}

對 JavaScript 函數比較熟悉的同學可能就知道該如何解決這種問題。是的,我們只需要定義一個函數,然後傳入需要變化的參數就可以了。

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

這種接收一些需要修改的參數,返回一個 Action 的函數在 Redux 中被稱為 Action Creators(動作創建器)。

當我們使用 Action Creators 來創建 Action 之後,我們再想要修改 Store 的狀態就變成了下面這樣:

dispatch(addTodo('我是一隻小小小圖雀'))

可以看到,我們的邏輯大大簡化了,每次發起一個新的 "ADD_TODO" action,都只需要傳入對應的 text。

與 React 整合

了解了 Action 的基礎概念之後,我們馬上來嘗試一下如何在 React 中發起更新動作。

首先,我們在 src 文件夾下面創建 actions 文件夾,然後在 actions 文件夾下創建 index.js 文件,並在裡面添加下面的 Action Creators:

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

因為在使用 Redux 的 React 應用中,我們將需要創建大量的 Action 或者 Action Creators,所以 Redux 社區的最佳實踐推薦我們創建一個獨立的 actions文件夾,並在這個文件夾裡面編寫特定的 Action 邏輯。

可以看到,我們加入了一個 addTodoAction Creator,它接收 text 參數,並每次自增一個 id,然後返回帶有 idtext ,並且類型為 "ADD_TODO" 的 Action。

接著我們修改 src/components/AddTodo.js 文件,將之前的 onSubmit 替換成以 dispatch(action) 的形式來修改 Store 的狀態:

import React from "react";  import { connect } from "react-redux";  import { addTodo } from "../actions";    const AddTodo = ({ dispatch }) => {    let input;      return (      <div>        <form          onSubmit={e => {            e.preventDefault();            if (!input.value.trim()) {              return;            }            dispatch(addTodo(input.value));            input.value = "";          }}        >          <input ref={node => (input = node)} />          <button type="submit">Add Todo</button>        </form>      </div>    );  };    export default connect()(AddTodo);

可以看到,上面的程式碼做了這幾項改變:

•首先我們從 react-redux 中導出了 connect 函數,它負責將 Store 中的狀態注入組件的同時,還給組件傳遞了一個額外的方法:dispatch,這樣我們就可以在組件的 props 中獲取這個方法。注意到我們在 AddTodo 函數式組件中使用了對象解構來獲取 dispatch 方法。•導出了我們剛剛創建的 addTodo Action Creators。•之後我們使用使用 addTodo 接收 input.value 輸入值,創建一個類型為 "ADD_TODO" 的 Action,並使用 dispatch 函數將這個 Action 發送給 Redux,請求更新 Store 的內容,更新 Store 的狀態需要 Reducers 來進行操作,我們將在 Reducer 中詳細講解它。

因為我們已經將直接修改 this.stateonSubmit 換成了 dispatch 一個 Action,所以我們刪除 src/components/App.js 相應的程式碼,因為我們現在已經不需要它們了:

import React from "react";  import AddTodo from "./AddTodo";  import TodoList from "./TodoList";  import Footer from "./Footer";    import { connect } from "react-redux";    // 省略 VisibilityFilters 和 getVisibleTodos ...    class App extends React.Component {    constructor(props) {      super(props);        this.toggleTodo = this.toggleTodo.bind(this);      this.setVisibilityFilter = this.setVisibilityFilter.bind(this);    }      toggleTodo(id) {      const { todos } = this.state;        this.setState({        todos: todos.map(todo =>          todo.id === id ? { ...todo, completed: !todo.completed } : todo        )      });    }      setVisibilityFilter(filter) {      this.setState({        filter: filter      });    }      render() {      const { todos, filter } = this.props;        return (        <div>          <AddTodo />          <TodoList            todos={getVisibleTodos(todos, filter)}            toggleTodo={this.toggleTodo}          />          <Footer            filter={filter}            setVisibilityFilter={this.setVisibilityFilter}          />        </div>      );    }  }    // 後面沒有變化 ...

可以看到我們刪除了 nextTodoId ,因為我們已經在 src/actions/index.js 中重新定義了它;接著我們刪除了 onSubmit 方法;最後我們刪除了傳遞給 AddTodo 組件的 onSubmit 方法。

保存修改的內容,我們在待辦事項小應用的輸入框裡面輸入點內容,然後點擊 Add Todo 按鈕,我們發現,之前的錯誤沒有再次出現。

留有遺憾的小結

在這一節中,我們完成了 Redux 狀態環形圖的第二個部分,即發起更新動作,我們首先講解了什麼是 Action 和 Action Creators,然後通過 dispatch(action) 的方式來發起一個更新 Store 中狀態的動作。

當我們使用了 dispatch(action) 之後,傳遞給子組件,用來修改父組件 State 的方法就不需要了,所以我們在程式碼中刪除了它們。在我們的 AddTodo 中,這個方法就是 onSubmit

但是有一點遺憾就是,我們雖然刪除了 onSubmit 方法,但是我們這一節中講到和實現的 dispatch(action) 還只能完成之前 onSubmit 方法的一半功能,即發起修改動作,但是我們目前還無法修改 Store 中的狀態。為了修改 Store 中的 State,我們需要定義 Reducers,用於響應我們 dispatch 的 Action,並根據 Action 的要求修改 Store 中對應的數據。

理解 Reducers: 響應 Action 的指令

在這一節中,我們馬上來了結上一節中留下的遺憾,即我們好像放了一聲空炮,dispatch 了一個 Action,但是沒有收穫任何效果。

首先祭出我們萬能的 Redux 狀態循環圖:

我們已經完成了前兩步了,離 Redux 整合進 React 只剩下最後一個步驟,即響應從組件中 dispatch 出來 Action,並更新 Store 中的狀態,這在 Redux 的概念中被稱之為 Reducers。

純化的 Reducers

reducer 是一個普通的 JavaScript 函數,它接收兩個參數:stateaction,前者為 Store 中存儲的那棵 JavaScript 對象狀態樹,後者即為我們在組件中 dispatch 的那個 Action。

reducer(state, action) {    // 對 state 進行操作    return newState;  }

reducer 根據 action 的指示,對 state 進行對應的操作,然後返回操作後的 state,Redux Store 會自動保存這份新的 state。

注意 Redux 官方社區對 reducer 的約定是一個純函數,即我們不能直接修改 state ,而是可以使用 {...} 等對象解構手段返回一個被修改後的新 state。 比如我們對 state = { a: 1, b: 2 } 進行修改,將 a 替換成 3,我們應該這麼做:newState = { ...state, a: 3 },而不應該 state.a = 3。這種不直接修改原對象,而是返回一個新對象的修改,我們稱之為 「純化」 的修改。

準備響應 Action 的修改

當了解了 Reducer 的概念之後,我們馬上在應用中響應我們之前 dispatch 的 Action,來彌補我們在上一節中留下的遺憾。

打開 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            }          ]        };      }      default:        return state;    }  };    // ...

上面的程式碼做了這麼幾項工作:

•可以看到,我們將之前的 rootReducer 進行改進,從單純地返回原來的 state,變成了一個 switch 語句,在 switch 語句中對 action 的 type 進行判斷,然後做出對應的處理。•當 action.type 的類型為 "ADD_TODO" 時,我們從 state 中取出了 todos ,然後使用 {...} 語法給 todos 添加一個新的元素對象,並設置 completed 屬性為 false 代表此 todo 未完成,最後再通過一層 {...} 語法將新的 todos 合併進老的 state 中,返回這個新的 state。•當 action.type 沒有匹配 switch 的任何條件時,我們返回默認的 state,表示 state 沒有任何更新。

當我們對 rootReducer 函數做了上述的改動之後,Redux 通過 Reducer 函數就可以響應從組件中 dispatch 出來的 action 了,目前我們還只可以響應 action.type"ADD_TODO" 的 action,它表示新增一個 todo。

保存修改的程式碼,打開瀏覽器,在輸入框裡面輸入點內容,然後點擊 Add Todo 按鈕,現在網頁應該可以正確響應你的操作了,我們又可以愉快地添加新的待辦事項了。

小結

在這一小節中,我們實現了第一個可以響應組件 dispatch 出來的 Action 的 Reducer,它判斷 action.type 的類型,並根據這些類型對 state 進行 「純化」 的修改,當 action.type 沒有匹配 Reducer 中任何類型時,我們返回原來的 state

當了解了 Redux 三大概念:Store,Action,Reducers 之後,我們再來看一張圖:

這張圖我們之前看過類似的,只不過這一次我們在這張圖上加了點東西,分別標出了 dispatchreducersconnect 所完成的工作。

dispatch(action) 用來在 React 組件中發出修改 Store 中保存狀態的指令。在我們需要新加一個待辦事項時,它取代了之前定義在組件中的 onSubmit 方法。•reducer(state, action) 用來根據這一指令修改 Store 中保存狀態對應的部分。在我們需要新加一個待辦事項時,它取代了之前定義在組件中的 this.setState 操作。•connect(mapStateToProps) 用來將更新好的數據傳給組件,然後觸發 React 重新渲染,顯示最新的狀態。它架設起 Redux 和 React 之間的數據通訊橋樑。

現在,Redux 的核心概念你已經全部學完了,並且我們的應用已經完全整合了 Redux。但是,我們還有一點工作沒有完成,那就是將整個應用完全使用 Redux 重構。在下一篇教程中,我們將使用我們在上面三節學到的知識,一步一步將我們的待辦事項應用的其他部分重構成 Redux,敬請期待~

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

References

[1] React 前端工程師學習路線: https://github.com/tuture-dev/react-roadmap [2] 入門教程: https://juejin.im/post/5df2f559e51d45584c553060 [3] 這篇教程: https://juejin.im/post/5df39f94518825122030859c [4] 使用 React 實現的待辦事項小應用: https://codesandbox.io/s/redux-quickstart-tutorial-4968k [5] 上篇教程: https://juejin.im/post/5df2f559e51d45584c553060 [6] 源碼地址: https://github.com/pftom/redux-quickstart-tutorial/tree/initial-code [7] 源碼地址: https://github.com/pftom/redux-quickstart-tutorial [8] 最終效果地址: https://codesandbox.io/s/redux-quickstart-tutorial-4968k [9] 最後效果地址: https://codesandbox.io/s/compassionate-shadow-7jruj [10] Create React App: https://create-react-app.dev/ [11] 上篇教程: https://juejin.im/post/5df2f559e51d45584c553060 [12] 圖雀社區: https://tuture.co/?utm_source=juejin_zhuanlan