[譯] 2019 React Redux 徹底指南

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

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

你能夠花幾個小時閱讀博客以及嘗試從複雜的「真實世界」應用中研習以將它拼湊起來。ios

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

接着咱們會看到更加進階的內容,手把手,直到你所有都理解爲止。咱們開始吧 :)github

請注意:本教程至關齊全。也就意味篇幅着比較長。我把它變成了一個完整的免費課程,而且我製做了精美的 PDF 你能夠在 iPad 或【任何 Android 設備】上閱讀。留下你的郵箱地址便可當即獲取。npm

視頻概述 Redux 要點

若是你更喜歡看視頻而不是閱讀,這個視頻涵蓋了如何在 React 應用中一步步添加 Redux:編程

視頻地址json

這與本教程的第一部分類似,咱們都會在一個簡單 React 應用中逐步地添加 Redux。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 添加。

在本地項目,你能夠經過 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前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索