Redux 包教包會(一):介紹 Redux 三大核心概念

tuture發表於2020-04-15

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

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

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

在我們閱讀教程之前

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

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

近來 React Hooks 確實很火,展現出驚人的潛力,甚至有人聲稱可以拋棄 Redux 了。其實筆者覺得這種說法不完全正確,Redux 的成功其實不僅僅是因為這個框架本身,還因為圍繞其構建起來的生態系統,比如 Enhancers、Middlewares、還有諸如 redux-form,redux-immutable 等,甚至還有基於 Redux 的上層框架,如 Dva 還有 Rematch,這些都為 Redux 鞏固了在 React 社群的王者地位。

而有趣的是,我們注意到 Redux 的 React 繫結庫 react-redux 現在正在用 React Hooks 重構,以求讓程式碼更加精煉和高效,所以筆者覺得 React Hooks 首先還處於萌芽階段,一小部分嚐鮮者在檢視使用它來構建更好的 React 專案或者框架,React Hooks 可以讓之前的這些專案和框架變得更好,以更好的輔助 Redux 生態的繼續繁榮,所以我們有理由相信,React Hooks 的出現,會讓 React 社群變得更加高效和專業,也會幫助 Redux 工具鏈變得更加輕量,最終作為 React 的一個優秀的特性將 React 和其生態帶向更好的遠方。

前提條件

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

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

你將學到什麼

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

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

程式碼和最終效果

本教程所實現的原始碼都託管在 Github 上:

你可以通過 CodeSandbox 檢視程式碼最終的效果:

開始 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 腳手架,它使用 Webpack Development Server(WDS)作為開發伺服器,因此在後面編輯程式碼的時候只需儲存檔案,我們的 React 應用就會自動重新整理,非常方便。

探索初始程式碼

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

我們希望展示一個 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 內容。

注意

connect 其實是一個高階函式,高階函式就是指可以接收引數呼叫並返回另外一個函式的函式。這裡 connect 通過接收 mapStateToProps 然後呼叫返回一個新函式,接著這個新函式再接收 App 元件作為引數,通過 mapStateToProps 注入 todosfilter 屬性,最後返回注入後的 App 元件。

提示

這裡之所以我們能在 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 邏輯。

可以看到,我們加入了一個 addTodo Action 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,歡迎繼續閱讀。

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

本作品採用《CC 協議》,轉載必須註明作者和本文連結

圖雀社群

相關文章