翻譯|A Dummy’s Guide to Redux and Thunk in React


title: 翻譯|A Dummy’s Guide to Redux and Thunk in React
date: 2017-04-15 21:36:07
categories: 翻譯
tags: Redux


Redux和Thunk的使用傻瓜教程

原文參見
Github repo
強(qiáng)烈推薦這篇文章.

如果你和我一樣,讀了Redux的文檔,看了Dan的錄像,Wes的課程,仍然不能抓住怎么使用Redux的核心,希望這個傻瓜教程能夠幫到你.
在真正實施之前,我做了一些嘗試,所以我決定寫一點(diǎn)從已經(jīng)存在的使用fetch JSON數(shù)據(jù)的app一步步轉(zhuǎn)變?yōu)槭褂肦edux和Redux Thunk的app.如果你不知道Thunk是什么,也不要擔(dān)心,我們會在”Redux 之路”部分來使用它執(zhí)行函數(shù)的異步調(diào)用.
這個教程需要你對React和ES6/2015有基本的掌握.

非Redux方法

components/ItemList.js中創(chuàng)建一個React組件,用于fetch和顯示items列表.

羅列一下基礎(chǔ)組件

首先我們使用包含各種items的state配置一個靜態(tài)的組件,2 個boolean state分別用于根據(jù)loading和error來渲染出各自的單獨(dú)顯示組件.

 import React, { Component } from 'react';
class ItemList extends Component {
  constructor() {
      super();
      this.state = {
          items: [//items在列表中現(xiàn)實的內(nèi)容
              {
                  id: 1,
                  label: 'List item 1'
              },
              {
                  id: 2,
                  label: 'List item 2'
              },
              {
                  id: 3,
                  label: 'List item 3'
              },
              {
                  id: 4,
                  label: 'List item 4'
              }
          ],
          hasErrored: false, //網(wǎng)絡(luò)請求錯誤的狀態(tài)
          isLoading: false   //網(wǎng)絡(luò)請求中的狀態(tài)
      };
  }
  render() {
      if (this.state.hasErrored) {//根據(jù)Errored的狀態(tài)決
      //定是否加載這個組件,網(wǎng)絡(luò)請求錯誤時,false=>true
          return <p>Sorry! There was an error loading the items</p>;
      }
      if (this.state.isLoading) {
      //網(wǎng)絡(luò)請求中的組件,發(fā)出請求時,false=>true
          return <p>Loading…</p>;
      }
      return (
          <ul>
              {this.state.items.map((item) => (
                  <li key={item.id}>
                      {item.label}
                  </li>
              ))}
          </ul>
      );
  }
}
export default ItemList;

可能看起來還不是特別能說明問題,但是已經(jīng)是一個好的開端了.
渲染的時候,組件輸出四個items列表,但是如果你把isLoadinghasError的state由false改為false的時候,對應(yīng)的<p></p>就會顯示出來.(注意每個組件都是return出來的,每次只顯示一個).

改為動態(tài)值

直接編碼items對于組件來說不是特別有用,所以最好從JSON API來fetch items數(shù)據(jù),如果這樣做的話,我們就可以把isLoadinghasError改為合適的狀態(tài).
響應(yīng)值和我們直接編碼是一樣,但是在實際生產(chǎn)中,你可能會拉回一個圖書暢銷榜的列表,最新的blog帖子或者其他app中需要的內(nèi)容.
為了fetch items,我們將使用合適的Fetch API.Fetch使得執(zhí)行請求比傳統(tǒng)的XMLHttpRequest更容易,并且返回的是響應(yīng)值的promise對象(這一點(diǎn)對于Thunk很重要).Fetch并不是在所有的瀏覽器中都可以使用,所以你需要在項目中添加依賴項.

 npm install whatwg-fetch --save

轉(zhuǎn)變實際上相當(dāng)簡單.

  • 首先把items的初始化state設(shè)置為空數(shù)組
  • 現(xiàn)在我們添加一個方法fetch數(shù)據(jù),同時還要設(shè)定loading和error的狀態(tài).
fetchData(url) {//fetch的包裝方法
  //進(jìn)入函數(shù)首先設(shè)定isLoading state false=>true
  this.setState({ isLoading: true });
  fetch(url)
      .then((response) => {//返回promise獨(dú)享
          if (!response.ok) {
              throw Error(response.statusText);
          }
          //不管返回的數(shù)據(jù)是什么,只要返回數(shù)據(jù)
          //就修改isLoading state true=>false
          this.setState({ isLoading: false });
          return response;
      })
      .then((response) => response.json())
      .then((items) => this.setState({ items })) 
      // ES6 property value shorthand for { items: items }
      .catch(() => this.setState({ hasErrored: true }));//返回數(shù)據(jù)的解析為json,如果捕獲到錯誤就hasErrored:
      // false=>true
}
  • 函數(shù)寫完以后,在組件加載的時候就調(diào)用函數(shù)
 componentDidMount() {
 this.fetchData('http://      5826ed963900d612000138bd.mockapi.io/items');
}

完成以后,代碼如下

 class ItemList extends Component {
   constructor() {
       this.state = {
           items: [],
       };
   }
   fetchData(url) {
       this.setState({ isLoading: true });
       fetch(url)
           .then((response) => {
               if (!response.ok) {
                   throw Error(response.statusText);
               }
               this.setState({ isLoading: false });
               return response;
           })
           .then((response) => response.json())
           .then((items) => this.setState({ items }))
           .catch(() => this.setState({ hasErrored: true }));
   }
   componentDidMount() {
       this.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
   }
   render() {
   }
}

差不多了,組件現(xiàn)在從REST API fetch items數(shù)據(jù),在4個items到達(dá)之前,你希望看”Loading...”出現(xiàn)提示.如果URL不能返回數(shù)據(jù),你應(yīng)該看到error 的信息.

然而(譯注:討厭的然而),在現(xiàn)實中,組件不應(yīng)該包含具體的fetch邏輯,data也不應(yīng)該儲存在組件的state中,所以Redux實時的出現(xiàn)了.

轉(zhuǎn)變到Redux

需要添加Redux, React Redux 和Redux Thunk作為依賴項.

 npm install redux react-redux redux-thunk --save

理解Redux

有幾個Redux的核心原理,我們必須要理解(譯注:話很簡單,但是要在大腦里構(gòu)建出Redux的工作流程是有花很多時間的和精力的).

  1. Redux中有一個全局state對象來管理整個應(yīng)用的state.在篇文章中,全局對象就是我們組件的初始化對象.
  2. 唯一能改變state的是觸發(fā)一個action,action是一個描述state應(yīng)該怎么改變的對象,Action Creators可以被dispatched的函數(shù),觸發(fā)一個變化,執(zhí)行的內(nèi)容是返回一個action
  3. 當(dāng)一個action被dispatch以后,Reducer執(zhí)行根據(jù)action的內(nèi)容實際改變state對象,如果action沒有找到匹配項,就會返回默認(rèn)的state.
  4. Reducers是純函數(shù),他們不能有任何的異步操作和mutate-必須要返回一個修改的copy.
  5. 單個的Reducer可以被combine為一個單一的rootReducer,從而創(chuàng)建一個離散的state聚合體.
  6. Store是把a(bǔ)ction和reducer組織到一起的工具,他包含了rootReducer代表的狀態(tài),中間件,允許你執(zhí)行實際的dispatchactions
  7. 為了在React中使用Redux,<Provider />組件包裝整個應(yīng)用,傳遞store到子代組件們.

設(shè)計我們的state

從我們現(xiàn)在已有的代碼里,可以知道我們的state需要3個屬性(properties):items,hasErrored,isLoading,這個三個屬性相應(yīng)的需要三個獨(dú)立的actions.

現(xiàn)在,這里講講為什么Action Creator和Action是不同的,他們也不是1:1的關(guān)系:我們需要第四個actiong creator來根據(jù)fetch data的不同狀態(tài)調(diào)用其他三個action(creators).這第四個action creator幾乎和我們原先的fetchData()一樣,但是它不會直接的使用this.setState({isLoading:true})來設(shè)置狀態(tài),我們將dispatch一個action去做同樣的事情:dispatch(isLoading(true)).

創(chuàng)建actions

在actions目錄下創(chuàng)建itmes.js文件,其中包含我們的action creators.創(chuàng)建三個簡單的actions.

    export function itemsHasErrored(bool) {
   return {
       type: 'ITEMS_HAS_ERRORED',
       hasErrored: bool
   };
}
export function itemsIsLoading(bool) {
   return {
       type: 'ITEMS_IS_LOADING',
       isLoading: bool
   };
}
export function itemsFetchDataSuccess(items) {
   return {
       type: 'ITEMS_FETCH_DATA_SUCCESS',
       items
   };
}
   
     
    ```



  



    

   
  
如前面提到的,action creators似能返回函數(shù)的函數(shù),使用`export`輸出單個action creators,便于在代碼中使用.
   第二個action creators接受一個布爾值(true/false)最為參數(shù),返回一個有意義的`type`和布爾值,分配合適的屬性.
   第三個,`itemsFetchSuccess()`,當(dāng)數(shù)據(jù)成功返回以后,傳遞數(shù)據(jù)作為`items`屬性的值.通過ES6的魔術(shù)屬性縮寫,我們能夠返回一個對象含有屬性名叫做`items`,他的值是`items`的數(shù)組.
   
  (Note: that the value you use for type and the name of the other property that is returned is important, because you will re-use them in your reducers)這一句不知道怎么翻譯.
  現(xiàn)在,我們有了三個actions,代表我們的狀態(tài),把原來的組件方法`fetchData`該給`itemFetchDaga()`action creator.
  默認(rèn)情況下,Redux action creators是不支持異步actions的,像是fetching data的操作,所以這里我們使用Redux Thunk.Thunk允許你在action creator里返回一個函數(shù)代替實際的action.內(nèi)部函數(shù)接受`dispatch`和`getState`作為參數(shù),但是我們僅僅使用`dispatch`.
  實際的簡單例子中五秒以后將會觸發(fā)`itemHasErrored()`函數(shù).
  
export function errorAfterFiveSeconds() {
// We return a function instead of an action object
//dispatch作為參數(shù)傳遞給胖箭頭函數(shù)
return (dispatch) => {
    setTimeout(() => {
        // This function is able to dispatch other action creators
        dispatch(itemsHasErrored(true));
    }, 5000);
};

}

現(xiàn)在我們知道thunk是什么了.編寫`itemsFetchData()`.

export function itemsFetchData(url) {
return (dispatch) => {
    //已進(jìn)入fetchdata,按順序把isLoading state 由
    // false=>true
    dispatch(itemsIsLoading(true));
    //fetch執(zhí)行實際的異步遠(yuǎn)程獲取數(shù)據(jù)操作
    fetch(url) 
        .then((response) => {
            if (!response.ok) {//根據(jù)狀態(tài)拋出錯誤
                throw Error(response.statusText);
            }
            //isLoading又改為false,加載Loading組件
            dispatch(itemsIsLoading(false));
            return response;
        })
        .then((response) => response.json())
        .then((items) => dispatch(itemsFetchDataSuccess(items)))
        .catch(() => 
        dispatch(itemsHasErrored(true)));
        //捕獲錯誤以后HasError的狀態(tài) false=>true
};

}


## 創(chuàng)建我們的reducers

action定義好了以后,可以編寫reducers接受actions,接著返回appliction的新狀態(tài)(譯注:實際上store中返回的對象都是一個新的對象,不是原對象的引用,這個就叫做immutable,facebook定義了一個immutable.js的技術(shù)實際是也是返回一個新的對象的硬拷貝,但是在原對象和修改對象之間共享了一部分內(nèi)容,這一點(diǎn)有點(diǎn)微妙).
注意:在Redux中,所有的reducers不考慮action,都會調(diào)用,所以即就是沒有action被應(yīng)用,你也必須要返回一個原來的定義的`state`.

每一個reducer接收兩個參數(shù),之前的state和一個`action`對象.也可以使用ES6的屬性來調(diào)用默認(rèn)的參數(shù)設(shè)定到默認(rèn)的`初始化state`.

在每個reducer內(nèi)部,使用`switch`申明來決定到底哪個`action.type`相匹配.如果是簡單的reducer,可能沒有必要使用`switch`,理論上使用`if/else`可能更快一點(diǎn).

如果`action.type`一點(diǎn)匹配,然后會返回和`action`相關(guān)的屬性.和前面提到的一樣,`type`和`action[屬性名]`是在action creators里定義的.

好啦,了解到這些內(nèi)容,在`reducers/item.js`中創(chuàng)建items reducers

export function itemsHasErrored(state = false, action) {
switch (action.type) {
case 'ITEMS_HAS_ERRORED':
return action.hasErrored;
default:
return state;
}
}
export function itemsIsLoading(state = false, action) {
switch (action.type) {
case 'ITEMS_IS_LOADING':
return action.isLoading;
default:
return state;
}
}
export function items(state = [], action) {
switch (action.type) {
case 'ITEMS_FETCH_DATA_SUCCESS':
return action.items;
default:
return state;
}
}


注意reducer根據(jù)結(jié)果store的state屬性來命名,`action.type`沒有必要想對應(yīng).前兩個表達(dá)完整的意思,第三個`items()`就稍微有點(diǎn)不同.

這是因為,可能會有很多條件返回`items`數(shù)組:有可能返回所有的數(shù)組,有可能在刪除dispatch以后返回`items`的次級結(jié)構(gòu).或者所有的items都被刪除了,會返回一個空數(shù)組.

為了重新遍歷,每一個reducer都會返回一個截然不同的state屬性,不需要考慮reducer內(nèi)部的條件到底有多少.剛開始花了我很長時間想明白這個問題.
單個的reducers創(chuàng)建好了以后,我們需要把單個的reducer合并(combine)成一個`rootReducer`,創(chuàng)建單一對象.

創(chuàng)建文件`reducers/index.js`

import { combineReducers } from 'redux';
import { items, itemsHasErrored, itemsIsLoading } from './items';
//由于每個reducer返回的都是一個對象
//所以這里的操作就是合并對象的操作,在underscore和loadsh
//里面可以找到合并js對象的代碼
export default combineReducers({
items,
itemsHasErrored,
itemsIsLoading
});

我們從`items`里導(dǎo)入每個reducers,使用redux的`combineReducers()`函數(shù)來合并輸出單一對象(譯注:所以每一個reducer返回的對象的屬性名應(yīng)該是唯一的,否則就覆蓋了,前面的內(nèi)容表達(dá)過這個意思)
因為我們的reducer的名字和在store中使用的屬性名一樣,所以我們可以使用ES6的對象字面量.

注意,我有意提到了reducer的前綴,所以當(dāng)我們的application變得比較復(fù)雜的時候,不能出現(xiàn)全局性的`hasErrored`和`isLoading`屬性.可以使用不同的error和loading state,所以前綴可以給你很大的靈活性.例如
import { combineReducers } from 'redux';

import { items, itemsHasErrored, itemsIsLoading } from './items';
import { posts, postsHasErrored, postsIsLoading } from './posts';
export default combineReducers({
items,
itemsHasErrored,
itemsIsLoading,
posts,
postsHasErrored,
postsIsLoading
});


替代方法是,可以在import的時候使用別名.但是我更愿意使用獨(dú)一無二的名字.

## 配置store,注入到你的app中

操作很直接,創(chuàng)建`store/configureStore.js`

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
export default function configureStore(initialState) {
return createStore(
rootReducer,
initialState,
applyMiddleware(thunk)
);
}


現(xiàn)在在index.js中包含`<Provider />`組件,`configureStore`,配置`store`.包裝app(`<ItemList />`),傳遞進(jìn)去`store`和`props`.

import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import configureStore from './store/configureStore';
import ItemList from './components/ItemList';
const store = configureStore(); // You can also pass in an initialState here
render(
<Provider store={store}>
<ItemList />
</Provider>,
document.getElementById('app')
);


我知道,其實花了很多努力才到了這一步,但是隨著設(shè)置的完成,我們就可以使用配置來操縱我們的組件了(譯注:這里是意譯,組件又稱為木偶組件,意思很清楚吧?誰是拿著吊線的人呢?就是redux).

## 把組件轉(zhuǎn)化為使用Redux store和方法

跳回到`components/ItemList.js`

在頂部導(dǎo)入需要的部分

import { connect } from 'react-redux';
import { itemsFetchData } from '../actions/items';


`connect`可以讓組件鏈接到Redux的store,`itemsFetchData`是在開始寫的action creator.我們僅僅需要導(dǎo)入actin creator.使用`dispatch`來觸發(fā)actions.(譯注:redux里面有很多內(nèi)容其實是很基礎(chǔ)的,例如這里,javascript的函數(shù)是一類對象,在js中函數(shù)是傳引用的,所以函數(shù)名可以作為函數(shù)的引用,通過另一函數(shù)的參數(shù)來傳遞. 厲害 ??).
在組件類定義好了以后,我們可以把Redux的state和action creator的dispatch映射到props上.
創(chuàng)建一個函數(shù),接受`state`返回一個props對象.在簡單的組件中,可以把前綴去掉.

const mapStateToProps = (state) => {
return {
items: state.items,
hasErrored: state.itemsHasErrored,
isLoading: state.itemsIsLoading
};
};


接著我們需要另一函數(shù)在props中`dispatch`我們的`itemsFetchData()`函數(shù).

const mapDispatchToProps = (dispatch) => {
return {
fetchData: (url) => dispatch(itemsFetchData(url))
};
};


還是把`item`前綴去掉.這里的fetchData函數(shù)接受一個`url`參數(shù),返回
派發(fā)調(diào)用`itemsFetchData(url)`操作.

現(xiàn)在,`mapStatetoProps()`和`mapDispatchToProps()`還不能做什么事情,需要改變一下代碼

export default connect(mapStateToProps, mapDispatchToProps)(ItemList);

`connect`函數(shù)把組件連接到Redux,同時映射了需要用到的props.

最后一步是使用props替換掉state.

* 刪掉`constructor(){}`和`fetchData(){}`
* 在`componentDidMount()`里把`this.fetchData()`改為`this.props.fetchData()`.
* `this.state.x` 統(tǒng)一改為`this.props.X`.

你的組件看起來應(yīng)該是

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { itemsFetchData } from '../actions/items';
class ItemList extends Component {
componentDidMount() {
this.props.fetchData('http://5826ed963900d612000138bd.mockapi.io/items');
}
render() {
if (this.props.hasErrored) {
return <p>Sorry! There was an error loading the items</p>;
}
if (this.props.isLoading) {
return <p>Loading…</p>;
}
return (
<ul>
{this.props.items.map((item) => (
<li key={item.id}>
{item.label}
</li>
))}
</ul>
);
}
}
const mapStateToProps = (state) => {
return {
items: state.items,
hasErrored: state.itemsHasErrored,
isLoading: state.itemsIsLoading
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: (url) => dispatch(itemsFetchData(url))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(ItemList);


好了,application現(xiàn)在使用Redux和Redux thunk來fetch和顯示數(shù)據(jù).

也不是太困難,是不是?

你現(xiàn)在也是Redux大師了 :D


## 接下來做什么?

我在github提交了每一步的代碼.我希望你克隆和理解他,然后添加根據(jù)items的id刪除item的功能.

我沒有真正提到Redux的內(nèi)容是,state是immutable,意識是你不能修改他.所以在reducers中每次都要返回一個新的state,上面的三個reducers比較簡單,剛剛好可以工作.但是如果是從items的數(shù)組中刪除操作,你可能不熟悉.
你不能再使用`Array.protype.splice()`來從數(shù)組中移除items.因為這樣做會mutate之前的state.[Dan在視頻中講解了怎么從數(shù)組中刪除一個元素](https://egghead.io/lessons/javascript-redux-avoiding-array-mutations-with-concat-slice-and-spread).如果你堅持要做,可以看看`delete-items`分支,可以找到解決辦法.

我真切的希望這篇文章講明了Redux和Thunk的原理以及怎么把現(xiàn)有的React application轉(zhuǎn)變到使用redux.寫作過程鞏固了我對Redux的理解(譯注:翻譯過程也鞏固了我的理解),所以我很高興這么做(譯注:so do I ??).


 下面有點(diǎn)不翻了。
 
 
 >我個人覺得這篇文章卻似寫的非常的好.強(qiáng)烈推薦.
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容