[譯] 2019 React Redux 完全指南

希裡花斯發表於2019-04-09

2019 React Redux 完全指南

A Complete Redux Tutorial (2019): why use it? - store - reducers - actions - thunks - data fetching

想要理解 Redux 完整的工作機制真的讓人頭疼。特別是作為初學者。

術語太多了!Actions、reducers、action creators、middleware、pure functions、immutability、thunks 等等。

怎麼把這些全都與 React 結合起來構建一個可執行的應用?

你可以花幾個小時閱讀部落格以及嘗試從複雜的“真實世界”應用中研習以將它拼湊起來。

在本篇 Redux 教程中,我會漸進地解釋如何將 Redux 與 React 搭配使用 —— 從簡單的 React 開始 —— 以及一個非常簡單的 React + Redux 案例。我會解釋為什麼每個功能都很有用(以及什麼情況下做取捨)。

接著我們會看到更加進階的內容,手把手,直到你全部都理解為止。我們開始吧 :)

請注意:本教程相當齊全。也就意味篇幅著比較長。我把它變成了一個完整的免費課程,並且我製作了精美的 PDF 你可以在 iPad 或【任何 Android 裝置】上閱讀。留下你的郵箱地址即可立即獲取。

視訊概述 Redux 要點

如果你更喜歡看視訊而不是閱讀,這個視訊涵蓋了如何在 React 應用中一步步新增 Redux:

視訊地址

這與本教程的第一部分相似,我們都會在一個簡單 React 應用中逐步地新增 Redux。

或者,繼續看下去!本教程不僅涵蓋視訊中的所有內容,還有其他乾貨。

你應該用 Redux 嗎?

都 9102 年了,弄清楚你是否還應該使用 Redux 十分必要。現在有更好的替代品出現嗎,使用 Hooks,Context 還是其他庫?

簡而言之:即使有很多替代品,Redux 仍舊不死。但是是否適用於你的應用,還得看具體場景。

超級簡單?只有一兩個地方需要用到幾個 state?元件內部 state 就很好了。你可以通過 classes,Hooks 或者二者一起來實現。

再複雜一點,有一些“全域性”的東西需要在整個應用中共享?Context API 可能完美適合你。

很多全域性的 state,與應用的各獨立部分都有互動?或者一個大型應用並且隨著時間推移只會越來越大?試試 Redux 吧。

你也可以以後再使用 Redux,不必在第一天就決定。從簡單開始,在你需要的時候再增加複雜性。

你知道 React 嗎?

React 可以脫離 Redux 單獨使用。Redux 是 React 的附加項

即使你打算同時使用它們,我還是強烈建議先脫離 Redux 學習純粹的 React。理解 props,state 以及單向資料流,在學習 Redux 之前先學習 “React 程式設計思想”。同時學習這兩個肯定會把你搞暈。

如果你想要入門 React ,我整理了一個為期 5 天的免費課程,教授所有基礎知識:

接下來的 5 天通過構建一些簡單的應用來學習 React。

第一課

Redux 的好處

如果你稍微使用過一段時間的 React,你可能就瞭解了 props 和單向資料流。資料通過 props 在元件樹間向傳遞。就像這個元件一樣:

Counter component

count 存在 App 的 state 裡,會以 prop 的形式向下傳遞:

Passing props down

要想資料向傳遞,需要通過回撥函式實現,因此必須首先將回撥函式向傳遞到任何想通過呼叫它來傳遞資料的元件中。

Passing callbacks down

你可以把資料想象成電流,通過彩色電線連線需要它的元件。資料通過線路上下流動,但是線路不能在空氣中貫穿 —— 它們必須從一個元件連線到另一個元件。

多級傳遞資料是一種痛苦

遲早你會陷入這類場景,頂級容器元件有一些資料,有一個 4 級以上的子元件需要這些資料。這有一個 Twitter 的例子,所有頭像都圈出來了:

Twitter user data

我們假設根元件 App 的 state 有 user 物件。該物件包含當前使用者頭像、暱稱和其他資料資訊。

為了把 user 資料傳遞給全部 3 個 Avatar 元件,必須要經過一堆並不需要該資料的中間元件。

Sending the user data down to the Avatar components

獲取資料就像用針在採礦探險一樣。等等,那根本沒有意義。無論如何,這很痛苦。也被稱為 “prop-drilling”。

更重要的是,這不是好的軟體設計。中間元件被迫接受和傳遞他們並不關心的 props。也就意味著重構和重用這些元件會變得比原本更難。

如果不需要這些資料的元件根本不用看到它們的話不是很棒嗎?

Redux 就是解決這個問題的一種方法。

相鄰元件間的資料傳遞

如果你有些兄弟元件需要共享資料,React 的方式是把資料向傳到父元件中,然後再通過 props 向下傳遞。

但這可能很麻煩。Redux 會為你提供一個可以儲存資料的全域性 "parent",然後你可以通過 React-Redux 把兄弟元件和資料 connect 起來。

使用 React-Redux 將資料連線到任何元件

使用 react-reduxconnect 函式,你可以將任何元件插入 Redux 的 store 以及取出需要的資料。

Connecting Redux to the Avatar components

Redux 還做了一些很酷的事情,比如使除錯更輕鬆(Redux DevTools 讓你檢查每一個 state 的變化),time-travel debugging(你可以回滾 state 變化,看看你的應用以前的樣子),從長遠來看,它讓程式碼變得更易於維護。它也會教你更多關於函數語言程式設計的知識。

內建 Redux 替代品

如果 Redux 對你來說太過繁瑣了,可以看看這些替代品。它們內建在 React 中。

Redux 替代品: The React Context API

在底層,React-Redux 使用 React 內建的 Context API 來傳遞資料。如果你願意,你可以跳過 Redux 直接使用 Context。你會錯過上面提到的 Redux 很棒的特性,但是如果你的應用很簡單並且想用簡單的方式傳遞資料,Context 就夠了。

既然你讀到這裡,我認為你真想學習 Redux,我不會在這裡比較 Redux 和 Context API 或者使用 Context使用 Reducer Hooks。你可以點選連結詳細瞭解。

如果你想深入研究 Context API,看我在 egghead 的課程 React Context 狀態管理

其他替代品:使用 children Prop

取決於你構建應用程式的方式,你可能會用更直接的方式把資料傳遞給子元件,那就是使用 children 和其他 props 結合的方式作為“插槽”。如果你組織的方式正確,就可以有效地跳過層次結構中的幾個層級。

我有一篇相關文章 “插槽”模式以及如何組織元件樹 來有效地傳遞資料。

學習 Redux,從簡單 React 開始

我們將採用增量的方法,從帶有元件 state 的簡單 React 應用開始,一點點新增 Redux,以及解決過程中遇到的錯誤。我們稱之為“錯誤驅動型開發” :)

這是一個計數器:

Counter component

這本例中,Counter 元件有 state,包裹著它的 App 是一個簡單包裝器。

Counter.js

import React from 'react';

class Counter extends React.Component {
  state = { count: 0 }

  increment = () => {
    this.setState({
      count: this.state.count + 1
    });
  }

  decrement = () => {
    this.setState({
      count: this.state.count - 1
    });
  }

  render() {
    return (
      <div>
        <h2>Counter</h2>
        <div>
          <button onClick={this.decrement}>-</button>
          <span>{this.state.count}</span>
          <button onClick={this.increment}>+</button>
        </div>
      </div>
    )
  }
}

export default Counter;
複製程式碼

快速回顧一下,它是如何執行的:

  • count state 儲存在 Counter 元件
  • 當使用者點選 "+" 時,會呼叫按鈕的 onClick 處理器執行 increment 函式。
  • increment 函式會更新 state 的 count 值。
  • 因為 state 改變了,React 會重新渲染 Counter 元件(以及它的子元素),這樣就會顯示新計數值。

如果你想要了解 state 變化機制的更多細節,去看 React 中的 state 可視指南然後再回到這裡。

不過說實話:如果上面內容對你來講不是複習的話,你需要在學 Redux 之前瞭解下 React 的 state 如何工作,否則你會巨困惑。參加我免費的 5 天 React 課程,用簡單的 React 獲得信心,然後再回到這裡。

跟上來!

最好的學習方式就是動手嘗試!所以這有個 CodeSandbox 你可以跟著做:

--> 在新 tab 中開啟 CodeSandbox

我強烈建議你將 CodeSandbox 與該教程保持同步並且隨著你進行時實際動手敲出這些例子。

在 React 應用中新增 Redux

在 CodeSandbox 中,展開左側的 Dependencies 選項,然後點選 Add Dependency。

搜尋 redux 新增依賴,然後再次點選 Add Dependency 搜尋 react-redux 新增。

[譯] 2019 React Redux 完全指南

在本地專案,你可以通過 Yarn 或者 NPM 安裝:npm install --save redux react-redux

redux vs react-redux

redux 給你一個 store,讓你可以在裡面儲存 state,取出 state,以及當 state 發生改變時做出響應。但那就是它所有能做的事。

實際上是 react-redux 把各個 state 和 React 元件連線起來。

沒錯:redux 對 React 根本不瞭解。

雖然,這兩個庫就像豆莢裡的兩個豌豆。99.999% 的情況下,當任何人在 React 的場景下提到 "Redux",他們指的是這兩個庫。因此當你在 StackOverflow、Reddit 或者其他地方看到 Redux 時,記住這一點。

redux 庫可以脫離 React 應用使用。它可以和 Vue、Angular 甚至後端的 Node/Express 應用一起使用。

Redux 有全域性唯一 Store

我們將首先從 Redux 中的一小部分入手:store。

我們已經討論過 Redux 怎樣在一個獨立 store 裡儲存你應用的 state。以及怎樣提取 state 的一部分把它作為 props 嵌入你的元件。

你會經常看到 "state" 和 "store" 這兩個詞互換使用。技術上來講,state 是資料,store 是儲存資料的地方。

因此:作為我們從簡單的 React 到 Redux 重構的第一步,我們要建立一個 store 來保持 state。

建立 Redux Store

Redux 有一個很方便的函式用來建立 stores,叫做 createStore。很合邏輯,嗯?

我們在 index.js 中建立一個 store。引入 createStore 然後像這樣呼叫:

index.js

import { createStore } from 'redux';

const store = createStore();

const App = () => (
  <div>
    <Counter/>
  </div>
);
複製程式碼

這樣會遇到 "Expected the reducer to be a function." 錯誤。

Error: Expected the reducer to be a function.

Store 需要一個 Reducer

因此,有件關於 Redux 的事:它並不是非常智慧。

你可能期待通過建立一個 store,它會給你的 state 一個合適的預設值。或許是一個空物件?

但是並非如此。這裡沒有約定優於配置。

Redux 不會對你的 state 做任何假設。它可能是一個 object、number、string,或者任何你需要的。這取決於你。

我們必須提供一個返回 state 的函式。這個函式被稱為 reducer(我們馬上就知道為什麼了)。那麼我們建立一個非常簡單的 reducer,把它傳給 createStore,然後看會發生什麼:

index.js

function reducer(state, action) {
  console.log('reducer', state, action);
  return state;
}

const store = createStore(reducer);
複製程式碼

修改完後,開啟控制檯(在 CodeSandbox 裡,點選底部的 Console 按鈕)。

你應該可以看到類似這樣的日誌資訊:

reducer undefined Object { type: @@redux/INIT }

(INIT 後面的字母和數字是 Redux 隨機生成的)

注意在你建立 store 的同時 Redux 如何呼叫你的 reducer。(為了證實這點:呼叫 createStore 之後立即輸出 console.log,看看 reducer 後面會列印什麼)

同樣注意 Redux 如何傳遞了一個 undefinedstate,同時 action 是一個有 type 屬性的物件。

我們稍後會更多地討論 actions。現在,我們先看看 reducer

Redux Reducer 是什麼?

"reducer" 術語看起來可能有點陌生和害怕,但是本節過後,我認為你會同意如下觀點,正如俗話所說的那樣,“只是一個函式”。

你用過陣列的 reduce 函式嗎?

它是這樣用的:你傳入一個函式,遍歷陣列的每一個元素時都會呼叫你傳入的函式,類似 map 的作用 —— 你可能在 React 裡面渲染列表而對 map 很熟悉。

你的函式呼叫時會接收兩個引數:上一次迭代的結果,和當前陣列元素。它結合當前元素和之前的 "total" 結果然後返回新的 total 值。

結合下面例子看會更加清晰明瞭:

var letters = ['r', 'e', 'd', 'u', 'x'];

// `reduce` 接收兩個引數:
//   - 一個用來 reduce 的函式 (也稱為 "reducer")
//   - 一個計算結果的初始值
var word = letters.reduce(
  function(accumulatedResult, arrayItem) {
    return accumulatedResult + arrayItem;
  },
''); // <-- 注意這個空字串:它是初始值

console.log(word) // => "redux"
複製程式碼

你給 reduce 傳入的函式理所應當被叫做 "reducer",因為它將整個陣列的元素 reduces 成一個結果。

Redux 基本上是陣列 reduce 的豪華版。前面,你看到 Redux reducers 如何擁有這個顯著特徵:

(state, action) => newState
複製程式碼

含義:它接收當前 state 和一個 action,然後返回 newState。看起來很像 Array.reduce 裡 reducer 的特點!

(accumulatedValue, nextItem) => nextAccumulatedValue
複製程式碼

Redux reducers 就像你傳給 Array.reduce 的函式作用一樣!:) 它們 reduce 的是 actions。它們把一組 actions(隨著時間)reduce 成一個單獨的 state。不同之處在於 Array 的 reduce 立即發生,而 Redux 則隨著正執行應用的生命週期一直髮生。

如果你仍然非常不確定,檢視下我的 [Redux reducers 工作機制]指南(daveceddia.com/what-is-a-r…)。不然的話,我們繼續向下看。

給 Reducer 一個初始狀態

記住 reducer 的職責是接收當前 state 和一個 action 然後返回新的 state。

它還有另一個職責:在首次呼叫的時候應該返回初始 state。它有點像應用的“引導頁”。它必須從某處開始,對吧?

慣用的方式是定義一個 initialState 變數然後使用 ES6 預設引數給 state 賦初始值。

既然要把 Counter state 遷移到 Redux,我們先立馬建立它的初始 state。在 Counter 元件裡,我們的 state 是一個有 count 屬性的物件,所以我們在這建立一個一樣的 initialState。

index.js

const initialState = {
  count: 0
};

function reducer(state = initialState, action) {
  console.log('reducer', state, action);
  return state;
}
複製程式碼

如果你再看下控制檯,你會看到 state 列印的值為 {count: 0}。那就是我們想要的。

所以這告訴我們一條關於 reducers 的重要規則。

Reducers 重要規則一:reducer 絕不能返回 undefined。

通常 state 應該總是已定義的。已定義的 state 是良好的 state。而定義的則那麼好(並且會破壞你的應用)。

Dispatch Actions 來改變 State

是的,一下來了兩個名字:我們將 "dispatch" 一些 "actions"。

什麼是 Redux Action?

在 Redux 中,具有 type 屬性的普通物件就被稱為 action。就是這樣,只要遵循這兩個規則,它就是一個 action:

{
  type: "add an item",
  item: "Apple"
}
複製程式碼

This is also an action:

{
  type: 7008
}
複製程式碼

Here's another one:

{
  type: "INCREMENT"
}
複製程式碼

Actions 的格式非常自由。只要它是個帶有 type 屬性的物件就可以了。

為了保證事務的合理性和可維護性,我們 Redux 使用者通常給 actions 的 type 屬性賦簡單字串,並且通常是大寫的,來表明它們是常量。

Action 物件描述你想做出的改變(如“增加 counter”)或者將觸發的事件(如“請求服務失敗並顯示錯誤資訊”)。

儘管 Actions 名聲響亮,但它是無趣的,呆板的物件。它們事實上不任何事情。反正它們自己不做。

為了讓 action 點事情,你需要 dispatch。

Redux Dispatch 工作機制

我們剛才建立的 store 有一個內建函式 dispatch。呼叫的時候攜帶 action,Redux 呼叫 reducer 時就會攜帶 action(然後 reducer 的返回值會更新 state)。

我們在 store 上試試看。

index.js

const store = createStore(reducer);
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "INCREMENT" });
store.dispatch({ type: "DECREMENT" });
store.dispatch({ type: "RESET" });
複製程式碼

在你的 CodeSandbox 中新增這些 dispatch 呼叫然後檢查控制檯

reducer undefined Object { type: @@redux/INIT }

每一次呼叫 dispatch 最終都會呼叫 reducer!

同樣注意到 state 每次都一樣?{count: 0} 一直沒變。

這是因為我們的 reducer 沒有作用於那些 actions。不過很容易解決。現在就開始吧。

在 Redux Reducer 中處理 Actions

為了讓 actions 做點事情,我們需要在 reducer 裡面寫幾行程式碼來根據每個 action 的 type 值來對應得更新 state。

有幾種方式實現。

你可以建立一個物件來通過 action 的 type 來查詢對應的處理函式。

或者你可以寫一大堆 if/else 語句

if(action.type === "INCREMENT") {
  ...
} else if(action.type === "RESET") {
  ...
}
複製程式碼

或者你可以用一個簡單的 switch 語句,也是我下面採用的方式,因為它很直觀,也是這種場景的常用方法。

儘管有些人討厭 switch,如果你也是 —— 隨意用你喜歡的方式寫 reducers 就好 :)

下面是我們處理 actions 的邏輯:

index.js

function reducer(state = initialState, action) {
  console.log('reducer', state, action);

  switch(action.type) {
    case 'INCREMENT':
      return {
        count: state.count + 1
      };
    case 'DECREMENT':
      return {
        count: state.count - 1
      };
    case 'RESET':
      return {
        count: 0
      };
    default:
      return state;
  }
}
複製程式碼

試一下然後在控制檯看看會輸出什麼。

reducer undefined Object { type: @@redux/INIT }

快看!count 變了!

我們準備好把它連線到 React 了,在此之前讓我們先談談這段 reducer 程式碼。

如何保持純 Reducers

另一個關於 reducers 的規則是它們必須是純函式。也就是說不能修改它們的引數,也不能有副作用(side effect)。

Reducer 規則二:Reducers 必須是純函式。

“副作用(side effect)”是指對函式作用域之外的任何更改。不要改變函式作用域以外的變數,不要呼叫其他會改變的函式(比如 fetch,跟網路和其他系統有關),也不要 dispatch actions 等。

技術角度來看 console.log 是副作用(side effect),但是我們忽略它。

最重要的事情是:不要修改 state 引數。

這意味著你不能執行 state.count = 0state.items.push(newItem)state.count++ 及其他型別的變動 —— 不要改變 state 本身,及其任何子屬性。

你可以把它想成一個遊戲,你唯一能做的事就是 return { ... }。這是個有趣的遊戲。開始會有點惱人。但是通過練習你會變得更好。

我整理了一個如何在 Redux 裡做 Immutable 更新完全指南,包含更新 state 中物件和陣列的七個通用模式。

安裝 Immer 在 reducers 裡面使用也是一種很好的方式。Immer 讓你可以像寫普通 mutable 程式碼一樣,最終會自動生成 immutable 程式碼。點選瞭解如何使用 Immer

建議:如果你是開始一個全新的應用程式,一開始就使用 Immer。它會為你省去很多麻煩。但是我向你展示這種困難方式是因為很多程式碼仍然採用這種方式,你一定會看到沒有用 Immer 寫的 reducers

全部規則

必須返回一個 state,不要改變 state,不要 connect 每一個元件,要吃西蘭花,11 點後不要外出…這簡直沒完沒了。就像一個規則工廠,我甚至不知道那是什麼。

是的,Redux 就像一個霸道的父母。但它是出於愛。函數語言程式設計的愛。

Redux 建立在不變性的基礎上,因為變化的全域性 state 是一條通往廢墟之路。

你試過在全域性物件裡面儲存你的 state 嗎?起初它還很好。美妙並且簡單。任何東西都能接觸到 state 因為它一直是可用的並且很容易更改。

然後 state 開始以不可預測的方式發生改變,想要找到改變它的程式碼變得幾乎不可能。

為了避免這些問題,Redux 提出了以下規則。

  • State 是隻讀的,唯一修改它的方式是 actions。
  • 更新的唯一方式:dispatch(action) -> reducer -> new state。
  • Reducer 函式必須是“純”的 —— 不能修改它的引數,也不能有副作用(side effect)。

如何在 React 中使用 Redux

此時我們有個很小的帶有 reducerstore,當接收到 action 時它知道如何更新 state

現在是時候將 Redux 連線到 React 了。

要做到這一點,要用到 react-redux 庫的兩樣東西:一個名為 Provider 的元件和一個 connect 函式。

通過用 Provider 元件包裝整個應用,如果它想的話,應用樹裡的每一個元件都可以訪問 Redux store。

index.js 裡,引入 Provider 然後用它把 App 的內容包裝起來。store 會以 prop 形式傳遞。

index.js

import { Provider } from 'react-redux';

...

const App = () => (
  <Provider store={store}>
    <Counter/>
  </Provider>
);
複製程式碼

這樣之後,CounterCounter 的子元素,以及子元素的子元素等等——所有這些現在都可以訪問 Redux stroe。

但不是自動的。我們需要在我們的元件使用 connect 函式來訪問 store。

React-Redux Provider 工作機制

Provider 可能看起來有一點點像魔法。它在底層實際是用了 React 的 Context 特性

Context 就像是連線每個元件的祕密通道,使用 connect 就可開啟祕密通道的大門。

想象一下,在一堆煎餅上澆糖漿以及它鋪滿所有煎餅的方式,即使你只在最上層倒了糖漿。Provider 對 Redux 做了同樣的事情。

為 Redux 準備 Counter 元件

現在 Counter 有了內部 state。我們打算把它幹掉,為從 Redux 以 prop 方式獲取 count 做準備。

移除頂部的 state 初始化,以及 incrementdecrement 內部呼叫的 setState。然後,把 this.state.count 替換成 this.props.count

Counter.js

class Counter extends React.Component {
  // state = { count: 0 }; // 刪除

  increment = () => {
    /*
    // 刪除
    this.setState({
      count: this.state.count + 1
    });
    */
  };

  decrement = () => {
    /*
    // 同樣刪除
    this.setState({
      count: this.state.count - 1
    });
    */
  };

  render() {
    return (
      <div className="counter">
        <h2>Counter</h2>
        <div>
          <button onClick={this.decrement}>-</button>
          <span className="count">{
            // 把 state:
            //// this.state.count
            // 替換成:
            this.props.count
          }</span>
          <button onClick={this.increment}>+</button>
        </div>
      </div>
    );
  }
}
複製程式碼

現在 incrementdecrement 是空的。我們會很快再次填充它們。

你會注意到 count 消失了 —— 它確實應該這樣,因為目前還沒有給 Counter 傳遞 count prop。

連線元件和 Redux

要從 Redux 獲取 count,我們首先需要在 Counter.js 頂部引入 connect 函式。

Counter.js

import { connect } from 'react-redux';
複製程式碼

然後我們需要在底部把 Counter 元件和 Redux 連線起來:

Counter.js

// 新增這個函式:
function mapStateToProps(state) {
  return {
    count: state.count
  };
}

// 然後把:
// export default Counter;

// 替換成:
export default connect(mapStateToProps)(Counter);
複製程式碼

之前我們只匯出了元件本身。現在我們用 connect 函式呼叫把它包裝起來,這樣我們就可以匯出已連線的 Counter。至於應用的其餘部分,看起來就像一個常規元件。

然後 count 應該就重新出現了!直到我們重新實現 increment/decrement,它是不會變化的。

如何使用 React Redux connect

你可能注意到這個呼叫看起來有點……奇怪。為什麼是 connect(mapStateToProps)(Counter) 而不是 connect(mapStateToProps, Counter) 或者 connect(Counter, mapStateToProps)?它做了什麼?

這樣寫是因為 connect 是一個高階函式,它簡單說就是當你呼叫它時會返回一個函式。然後呼叫返回的函式傳入一個元件時,它會返回一個新(包裝的)元件。

它的另一個名稱是 高階元件 (簡稱 "HOC")。HOCs 過去曾有過一些糟糕的新聞,但它仍然是一個相當有用的模式,connect 就是一個很好的例子。

Connect 做的是在 Redux 內部 hook,取出整個 state,然後把它傳進你提供的 mapStateToProps 函式。它是個自定義函式,因為只有知道你存在 Redux 裡面的 state 的“結構”。

mapStateToProps 工作機制

connect 把整個 state 傳給了你的 mapStateToProps 函式,就好像在說,“嘿,告訴我你想從這堆東西里面要什麼。”

mapStateToProps 返回的物件以 props 形式傳給了你的元件。以上面為例就是把 state.count 的值用 count prop 傳遞:物件的屬性變成了 prop 名稱,它們對應的值會變成 props 的值。你看,這個函式就像字面含義一樣定義從 state 到 props 的對映

順便說說 —— mapStateToProps 的名稱是使用慣例,但並不是特定的。你可以簡寫成 mapState 或者用任何你想的方式呼叫。只要你接收 state 物件然後返回全是 props 的物件,那就沒問題。

為什麼不傳整個 state?

在上面的例子中,我們的 state 結構已經是對的了,看起來 mapDispatchToProps 可能是不必要的。如果你實質上覆制引數(state)給一個跟 state 相同的物件,這有什麼意義呢?

在很小的例子中,可能會傳全部 state,但通常你只會從更大的 state 集合中選擇部分元件需要的資料。

並且,沒有 mapStateToProps 函式,connect 不會傳遞任何 state。

可以傳整個 state,然後讓元件梳理。但那不是一個很好的習慣,因為元件需要知道 Redux state 的結構然後從中挑選它需要的資料,後面如果你想更改結構會變得更難。

從 React 元件 Dispatch Redux Actions

現在我們的 Counter 已經被 connect 了,我們也獲取到了 count 值。現在我們如何 dispatch actions 來改變 count?

好吧,connect 為你提供支援:除了傳遞(mapped)state,它從 store 傳遞了 dispatch 函式!

要在 Counter 內部 dispatch action,我們可以呼叫 this.props.dispatch 攜帶一個 action。

我們的 reducer 已經準備好處理 INCREMENTDECREMENT actions 了,那麼接下來從 increment/decrement 中 dispatch:

Counter.js

increment = () => {
  this.props.dispatch({ type: "INCREMENT" });
};

decrement = () => {
  this.props.dispatch({ type: "DECREMENT" });
};
複製程式碼

現在我們完成了。按鈕應該又重新生效了。

試試這個!加一個重置按鈕

這有個小練習:給 counter 新增“重置”按鈕,點選時 dispatch "RESET" action。

Reducer 已經寫好處理這個 action,因此你只需要修改 Counter.js。

Action 常量

在大部分 Redux 應用中,你可以看到 action 常量都是一些簡單字串。這是一個額外的抽象級別,從長遠來看可以為你節省不少時間。

Action 常量幫你避免錯別字,action 命名的錯別字會是一個巨大的痛苦:沒有報錯,沒有哪裡壞掉的明顯標誌,並且你的 action 沒有做任何事情?那就可能是個錯別字。

Action 常量很容易編寫:用變數儲存你的 action 字串。

把這些變數放在一個 actions.js 檔案裡是個好辦法(當你的應用很小時)。

actions.js

export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";
複製程式碼

然後你就可以引入這些 action 名稱,用它們來代替手寫字串:

Counter.js

import React from "react";
import { INCREMENT, DECREMENT } from './actions';

class Counter extends React.Component {
  state = { count: 0 };

  increment = () => {
    this.props.dispatch({ type: INCREMENT });
  };

  decrement = () => {
    this.props.dispatch({ type: DECREMENT });
  };

  render() {
    ...
  }
}
複製程式碼

Redux Action 生成器是什麼?

現在我們已經手寫 action 物件。像個異教徒。

如果你有一個函式會為你編寫它會怎麼樣?不要再誤寫 actinos 了!

我可以告訴你,這很瘋狂。手寫 { type: INCREMENT } 並保證沒有弄亂有多困難?

當你的應用變得越來越大,actions 越來越多,並且這些 actions 開始變得更復雜 —— 要傳更多資料而不僅是一個 type —— action 生成器會幫上大忙。

就像 action 常量一樣,但它們不是必須品。這是另一層的抽象,如果你不想在你的應用裡面使用,那也沒關係。

不過我還是會解釋下它們是什麼。然後你可以決定你是否有時/總是/絕不想使用它們。

Actions 生成器在 Redex 術語中是一個簡單的函式術語,它返回一個 action 物件。就這些 :)

這是其中兩個,返回熟悉的 actions。順便說一句,它們在 action 常量的 "actions.js" 中完美契合。

actions.js

export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";

export function increment() {
  return { type: INCREMENT };
}

export const decrement = () => ({ type: DECREMENT });
複製程式碼

我用了兩種不同方式——一個 function 和一個箭頭函式——來表明你用哪種方式寫並不重要。挑選你喜歡的方式就好。

你可能注意到函式命名是小寫的(好吧,如果較長的話會是駝峰命名),而 action 常量會是 UPPER_CASE_WITH_UNDERSCORES。同樣,這也只是慣例。這會讓你一眼區分 action 生成器和 action 常量。但你也可以按你喜歡的方式命名。Redux 並不關心。

現在,如何使用 action 生成器呢?引入然後 dispatch 就好了,當然!

Counter.js

import React from "react";
import { increment, decrement } from './actions';

class Counter extends React.Component {
  state = { count: 0 };

  increment = () => {
    this.props.dispatch(increment()); // << 在這使用
  };

  decrement = () => {
    this.props.dispatch(decrement());
  };

  render() {
    ...
  }
}
複製程式碼

關鍵是要記得呼叫 action creator()!

不要 dispatch(increment) ?

應該 dispatch(increment())

牢記 action 生成器是一個平凡無奇的函式。Dispatch 需要 action 是一個物件,而不是函式。

而且:你肯定會在這裡出錯並且非常困惑。至少一次,或許很多次。那很正常。我有時也依舊會忘記。

如何使用 React Redux mapDispatchToProps

現在你知道 action 生成器是什麼,我們可以討論又一個級別的抽象。(我知道,我知道。這是可選的。)

你知道 connect 如何傳遞 dispatch 函式嗎?你知道你是如何厭倦一直敲 this.props.dispatch 並且它看起來多麼混亂?(跟我來)

寫一個 mapDispatchToProps 物件(或者函式!但通常是物件)然後傳給你要包裝元件的 connect 函式,你將收到這些 action 生成器作為可呼叫 props。看程式碼:

Counter.js

import React from 'react';
import { connect } from 'react-redux';
import { increment, decrement } from './actions';

class Counter extends React.Component {
  increment = () => {
    // 我們可以呼叫 `increment` prop,
    // 它會 dispatch action:
    this.props.increment();
  }

  decrement = () => {
    this.props.decrement();
  }

  render() {
    // ...
  }
}

function mapStateToProps(state) {
  return {
    count: state.count
  };
}

// 在這個物件中, 屬性名會成為 prop 的 names,
// 屬性值應該是 action 生成器函式.
// 它們跟 `dispatch` 繫結起來.
const mapDispatchToProps = {
  increment,
  decrement
};

export default connect(mapStateToProps, mapDispatchToProps)(Counter);
複製程式碼

這很棒,因為它把你從手動呼叫 dispatch 中解放出來。

你可以把 mapDispatch 寫成一個函式,但是物件能滿足 95% 你所需的場景。詳細內容請看 函式式 mapDispatch 以及為什麼你可能並不需要它

如何使用 Redux Thunk 獲取資料

既然 reducers 應該是“純”的,我們不能做在 reducer 裡面做任何 API 呼叫或者 dispatch actions。

我們也不能在 action 生成器裡面做這些事!

但是如果我們把 action 生成器返回一個可以處理我們工作的函式會怎樣呢?就像這樣:

function getUser() {
  return function() {
    return fetch('/current_user');
  };
}
複製程式碼

越界了,Redux 不支援這種 actions。固執的 Redux 只接受簡單物件作為 actions。

這時就需要 redux-thunk 了。它是個中介軟體,基本是 Redux 的一個外掛,它可以使 Redux 處理像上面 getUser() 那樣的 actions。

你可以像其他 action 生成器一樣 dispatch 這些 "thunk actions":dispatch(getUser())

"thunk" 是什麼?

"thunk" 是(少見)指被其它函式作為返回值的函式

在 Redux 術語中,它是一個返回值為函式而非簡單 action 物件的 action 生成器,就像這樣:

function doStuff() {
  return function(dispatch, getState) {
    // 在這裡 dispatch actions
    // 或者獲取資料
    // 或者該幹啥幹啥
  }
}
複製程式碼

從技術角度講,被返回的函式就是 "thunk",把它作為返回值的就是“action 生成器”。通常我把它們一起稱為 "thunk action"。

Action 生成器返回的函式接收兩個引數:dispatch 函式和 getState

大多數場景你只需要 dispatch,但有時你想根據 Redux state 裡面的值額外做些事情。這種情況下,呼叫 getState() 你就會獲得整個 state 的值然後按需所取。

如何安裝 Redux Thunk

使用 NPM 或者 Yarn 安裝 redux-thunk,執行 npm install --save redux-thunk

然後,在 index.js(或者其他你建立 store 的地方),引入 redux-thunk 然後通過 Redux 的 applyMiddleware 函式把它應用到 store 中。

import thunk from 'redux-thunk';
import { createStore, applyMiddleware } from 'redux';

function reducer(state, action) {
  // ...
}

const store = createStore(
  reducer,
  applyMiddleware(thunk)
);
複製程式碼

必須確保 thunk 包裝在 applyMiddleware 呼叫裡面,否則不會生效。不要直接傳 thunk

結合 Redux 請求資料的例子

設想一下你想展示一個產品列表。你已經獲得了後端 API 可以響應 GET /products,所以你建立了一個 thunk action 來從後端請求資料:

productActions.js

export function fetchProducts() {
  return dispatch => {
    dispatch(fetchProductsBegin());
    return fetch("/products")
      .then(res => res.json())
      .then(json => {
        dispatch(fetchProductsSuccess(json.products));
        return json.products;
      })
      .catch(error => dispatch(fetchProductsFailure(error)));
  };
}
複製程式碼

fetch("/products") 是實際上請求資料的部分。然後我們在它前後分別做了一些 dispatch 呼叫。

Dispatch Action 來獲取資料

要開始呼叫並且實際獲取資料,我們需要 dispatch fetchProducts action。

在哪裡呼叫呢?

如果某一特定的元件需要資料,最好的呼叫地方通常是在元件剛剛載入之後,也就是它的 componentDidMount 生命週期函式。

或者,如果你在使用 Hooks,useEffect hook 裡面也是個好地方。

有時你要獲取整個應用都需要的真正的全域性資料 —— 如“使用者資訊”或者“國際化”。這種場景,就在你建立 store 後使用 store.dispatch 來 dispatch action,而不是等待元件載入後。

如何給 Redux Actions 命名

獲取資料的 Redux actions 通常使用標準三連:BEGIN、SUCCESS、FAILURE。這不是硬性要求,只是慣例。

BEGIN/SUCCESS/FAILURE 模式很棒,因為它給你提供鉤子來跟蹤發生了什麼 —— 比如,設定 "loading" 標誌為 "true" 以響應 BEGIN 操作,在 SUCCESS 或 FAILURE 之後設為 false

而且,與 Redux 中的其他所有內容一樣,這個也是一個慣例,如果你不需要的話可以忽略掉。

在你呼叫 API 之前,dispatch BEGIN action。

呼叫成功之後,你可以 dispatch SUCCESS 資料。如果請求失敗,你可以 dispatch 錯誤資訊。

有時最後一個呼叫 ERROR。其實呼叫什麼一點也不重要,只要你保持一致就好。

注意:dispatch Error action 來處理 FAILURE 會導致你跟蹤程式碼的時候毫無頭緒,知道 action 正確 dispatch 但是資料卻沒更新。吸取我的教訓 :)

這是那幾個 actions,以及它們的 action 生成器:

productActions.js

export const FETCH_PRODUCTS_BEGIN   = 'FETCH_PRODUCTS_BEGIN';
export const FETCH_PRODUCTS_SUCCESS = 'FETCH_PRODUCTS_SUCCESS';
export const FETCH_PRODUCTS_FAILURE = 'FETCH_PRODUCTS_FAILURE';

export const fetchProductsBegin = () => ({
  type: FETCH_PRODUCTS_BEGIN
});

export const fetchProductsSuccess = products => ({
  type: FETCH_PRODUCTS_SUCCESS,
  payload: { products }
});

export const fetchProductsFailure = error => ({
  type: FETCH_PRODUCTS_FAILURE,
  payload: { error }
});
複製程式碼

接收到 FETCH_PRODUCTS_SUCCESS action 返回的產品資料後,我們寫一個 reducer 把它存進 Redux store 中。開始請求時把 loading 標誌設為 true,失敗或者完成時設為 false。

productReducer.js

import {
  FETCH_PRODUCTS_BEGIN,
  FETCH_PRODUCTS_SUCCESS,
  FETCH_PRODUCTS_FAILURE
} from './productActions';

const initialState = {
  items: [],
  loading: false,
  error: null
};

export default function productReducer(state = initialState, action) {
  switch(action.type) {
    case FETCH_PRODUCTS_BEGIN:
      // 把 state 標記為 "loading" 這樣我們就可以顯示 spinner 或者其他內容
      // 同樣,重置所有錯誤資訊。我們從新開始。
      return {
        ...state,
        loading: true,
        error: null
      };

    case FETCH_PRODUCTS_SUCCESS:
      // 全部完成:設定 loading 為 "false"。
      // 同樣,把從服務端獲取的資料賦給 items。
      return {
        ...state,
        loading: false,
        items: action.payload.products
      };

    case FETCH_PRODUCTS_FAILURE:
      // 請求失敗,設定 loading 為 "false".
      // 儲存錯誤資訊,這樣我們就可以在其他地方展示。
      // 既然失敗了,我們沒有產品可以展示,因此要把 `items` 清空。
      //
      // 當然這取決於你和應用情況:
      // 或許你想保留 items 資料!
      // 無論如何適合你的場景就好。
      return {
        ...state,
        loading: false,
        error: action.payload.error,
        items: []
      };

    default:
      // reducer 需要有 default case。
      return state;
  }
}
複製程式碼

最後,我們需要把產品資料傳給展示它們並且也負責請求資料的 ProductList 元件。

ProductList.js

import React from "react";
import { connect } from "react-redux";
import { fetchProducts } from "/productActions";

class ProductList extends React.Component {
  componentDidMount() {
    this.props.dispatch(fetchProducts());
  }

  render() {
    const { error, loading, products } = this.props;

    if (error) {
      return <div>Error! {error.message}</div>;
    }

    if (loading) {
      return <div>Loading...</div>;
    }

    return (
      <ul>
        {products.map(product =>
          <li key={product.id}>{product.name}</li>
        )}
      </ul>
    );
  }
}

const mapStateToProps = state => ({
  products: state.products.items,
  loading: state.products.loading,
  error: state.products.error
});

export default connect(mapStateToProps)(ProductList);
複製程式碼

我指的是帶有 state.products.<whatever> 的資料而不僅僅是 state.<whatever>,因為我假設你可能會有不止一個 reducer,每一個都處理各自的 state。為了確保這樣,我們可以寫一個 rootReducer.js 檔案把它們放在一起:

rootReducer.js

import { combineReducers } from "redux";
import products from "./productReducer";

export default combineReducers({
  products
});
複製程式碼

然後,當我們建立 store 我們可以傳遞這個“根” reducer:

index.js

import rootReducer from './rootReducer';

// ...

const store = createStore(rootReducer);
複製程式碼

Redux 中錯誤處理

這裡的錯誤處理比較輕量,但是對大部分呼叫 API 的 actions 來說基本結構是一樣的。基本觀點是:

  1. 當呼叫失敗時,dispatch 一個 FAILURE action
  2. 通過設定一些標誌變數和/或儲存錯誤資訊來處理 reducer 中的 FAILURE action。
  3. 把錯誤標誌和資訊(如果有的話)傳給需要處理錯誤的元件,然後根據任何你覺得合適的方式渲染錯誤資訊。

能避免重複渲染嗎?

這確實個常見問題。是的,它不止一次觸發渲染。

它首先會渲染空 state,然後再渲染 loading state,接著會再次渲染展示產品。可怕!三次渲染!(如果你直接跳過 "loading" state 就可以把渲染次數將為兩次)

你可能會擔心不必要的渲染影響效能,但是不會:單次渲染非常快。如果你在開發的應用肉眼可見的慢的話,分析一下找出慢的原因。

這樣想吧:當沒有商品或者正在載入或者發生錯誤的時候應用需要展示一些東西。在資料準備好之前,你可能不想只展示一個空白螢幕。這給你了一個提供良好使用者體驗的機會。

接下來呢?

希望這篇教程能幫你更加理解 Redux!

如果你想深入瞭解裡面的細節,Redux 文件有很多很好的例子。Mark Erikson (Redux 維護者之一)的部落格有一個不錯的常用的 Redux 系列

下週,我會釋出一個新課程,Pure Redux,涵蓋這裡的所有內容,豐富了更多細節:

  • 如何做 immutable 更新
  • 使用 Immer 輕鬆實現 immutable
  • 使用 Redux DevTools 除錯應用
  • 為 reducers、actions 和 thunk actions 編寫單元測試

還有一整個模組講解我們建立一個完整的應用,從開始到結束,包含這些:

  • 將 CRUD 操作與 Redux 整合 —— 增刪查改
  • 建立 API 服務
  • 可訪問路由以及路由載入時請求資料
  • 處理模態對話方塊
  • 將多個 reducer 與 combineReducers 結合使用
  • 如何使用選擇器以及 reselect 以提高效能和可維護性
  • 許可權和 session 管理
  • 管理員和普通使用者檢視分離

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章