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
强烈推荐这篇文章.
如果你和我一样,读了Redux的文档,看了Dan的录像,Wes的课程,仍然不能抓住怎么使用Redux的核心,希望这个傻瓜教程能够帮到你.
在真正实施之前,我做了一些尝试,所以我决定写一点从已经存在的使用fetch JSON数据的app一步步转变为使用Redux和Redux Thunk的app.如果你不知道Thunk是什么,也不要担心,我们会在”Redux 之路”部分来使用它执行函数的异步调用.
这个教程需要你对React和ES6/2015有基本的掌握.
非Redux方法
在components/ItemList.js
中创建一个React组件,用于fetch和显示items列表.
罗列一下基础组件
首先我们使用包含各种items的state配置一个静态的组件,2 个boolean state分别用于根据loading和error来渲染出各自的单独显示组件.
import React, { Component } from 'react';
class ItemList extends Component {
constructor() {
super();
this.state = {
items: [//items在列表中现实的内容
{
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, //网络请求错误的状态
isLoading: false //网络请求中的状态
};
}
render() {
if (this.state.hasErrored) {//根据Errored的状态决
//定是否加载这个组件,网络请求错误时,false=>true
return Sorry! There was an error loading the items
;
}
if (this.state.isLoading) {
//网络请求中的组件,发出请求时,false=>true
return Loading…
;
}
return (
{this.state.items.map((item) => (
-
{item.label}
))}
);
}
}
export default ItemList;
可能看起来还不是特别能说明问题,但是已经是一个好的开端了.
渲染的时候,组件输出四个items列表,但是如果你把isLoading
和hasError
的state由false改为false的时候,对应的就会显示出来.(注意每个组件都是return出来的,每次只显示一个).
改为动态值
直接编码items对于组件来说不是特别有用,所以最好从JSON API来fetch items
数据,如果这样做的话,我们就可以把isLoading
和hasError
改为合适的状态.
响应值和我们直接编码是一样,但是在实际生产中,你可能会拉回一个图书畅销榜的列表,最新的blog帖子或者其他app中需要的内容.
为了fetch items,我们将使用合适的Fetch API.Fetch使得执行请求比传统的XMLHttpRequest更容易,并且返回的是响应值的promise对象(这一点对于Thunk很重要).Fetch并不是在所有的浏览器中都可以使用,所以你需要在项目中添加依赖项.
npm install whatwg-fetch --save
转变实际上相当简单.
- 首先把
items
的初始化state设置为空数组 - 现在我们添加一个方法fetch数据,同时还要设定loading和error的状态.
fetchData(url) {//fetch的包装方法
//进入函数首先设定isLoading state false=>true
this.setState({ isLoading: true });
fetch(url)
.then((response) => {//返回promise独享
if (!response.ok) {
throw Error(response.statusText);
}
//不管返回的数据是什么,只要返回数据
//就修改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 }));//返回数据的解析为json,如果捕获到错误就hasErrored:
// false=>true
}
- 函数写完以后,在组件加载的时候就调用函数
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() {
}
}
差不多了,组件现在从REST API fetch items
数据,在4个items到达之前,你希望看”Loading...”出现提示.如果URL不能返回数据,你应该看到error 的信息.
然而(译注:讨厌的然而),在现实中,组件不应该包含具体的fetch逻辑,data也不应该储存在组件的state中,所以Redux实时的出现了.
转变到Redux
需要添加Redux, React Redux 和Redux Thunk作为依赖项.
npm install redux react-redux redux-thunk --save
理解Redux
有几个Redux的核心原理,我们必须要理解(译注:话很简单,但是要在大脑里构建出Redux的工作流程是有花很多时间的和精力的).
- Redux中有一个全局state对象来管理整个应用的state.在篇文章中,全局对象就是我们组件的初始化对象.
- 唯一能改变state的是触发一个action,action是一个描述state应该怎么改变的对象,Action Creators可以被
dispatch
ed的函数,触发一个变化,执行的内容是返回一个action - 当一个action被dispatch以后,Reducer执行根据action的内容实际改变state对象,如果action没有找到匹配项,就会返回默认的state.
-
Reducers是纯函数,他们不能有任何的异步操作和mutate-必须要返回一个修改的copy
. - 单个的Reducer可以被combine为一个单一的
rootReducer
,从而创建一个离散的state聚合体. - Store是把action和reducer组织到一起的工具,他包含了
rootReducer
代表的状态,中间件,允许你执行实际的dispatch
actions - 为了在React中使用Redux,
组件包装整个应用,传递store
到子代组件们.
设计我们的state
从我们现在已有的代码里,可以知道我们的state需要3个属性(properties):items
,hasErrored
,isLoading
,这个三个属性相应的需要三个独立的actions.
现在,这里讲讲为什么Action Creator和Action是不同的,他们也不是1:1的关系:我们需要第四个actiong creator来根据fetch data的不同状态调用其他三个action(creators).这第四个action creator几乎和我们原先的fetchData()
一样,但是它不会直接的使用this.setState({isLoading:true})
来设置状态,我们将dispatch
一个action去做同样的事情:dispatch(isLoading(true))
.
创建actions
在actions目录下创建itmes.js文件,其中包含我们的action creators.创建三个简单的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似能返回函数的函数,使用`export`输出单个action creators,便于在代码中使用.
第二个action creators接受一个布尔值(true/false)最为参数,返回一个有意义的`type`和布尔值,分配合适的属性.
第三个,`itemsFetchSuccess()`,当数据成功返回以后,传递数据作为`items`属性的值.通过ES6的魔术属性缩写,我们能够返回一个对象含有属性名叫做`items`,他的值是`items`的数组.
(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)这一句不知道怎么翻译.
现在,我们有了三个actions,代表我们的状态,把原来的组件方法`fetchData`该给`itemFetchDaga()`action creator.
默认情况下,Redux action creators是不支持异步actions的,像是fetching data的操作,所以这里我们使用Redux Thunk.Thunk允许你在action creator里返回一个函数代替实际的action.内部函数接受`dispatch`和`getState`作为参数,但是我们仅仅使用`dispatch`.
实际的简单例子中五秒以后将会触发`itemHasErrored()`函数.
export function errorAfterFiveSeconds() {
// We return a function instead of an action object
//dispatch作为参数传递给胖箭头函数
return (dispatch) => {
setTimeout(() => {
// This function is able to dispatch other action creators
dispatch(itemsHasErrored(true));
}, 5000);
};
}
现在我们知道thunk是什么了.编写`itemsFetchData()`.
export function itemsFetchData(url) {
return (dispatch) => {
//已进入fetchdata,按顺序把isLoading state 由
// false=>true
dispatch(itemsIsLoading(true));
//fetch执行实际的异步远程获取数据操作
fetch(url)
.then((response) => {
if (!response.ok) {//根据状态抛出错误
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的状态 false=>true
};
}
## 创建我们的reducers
action定义好了以后,可以编写reducers接受actions,接着返回appliction的新状态(译注:实际上store中返回的对象都是一个新的对象,不是原对象的引用,这个就叫做immutable,facebook定义了一个immutable.js的技术实际是也是返回一个新的对象的硬拷贝,但是在原对象和修改对象之间共享了一部分内容,这一点有点微妙).
注意:在Redux中,所有的reducers不考虑action,都会调用,所以即就是没有action被应用,你也必须要返回一个原来的定义的`state`.
每一个reducer接收两个参数,之前的state和一个`action`对象.也可以使用ES6的属性来调用默认的参数设定到默认的`初始化state`.
在每个reducer内部,使用`switch`申明来决定到底哪个`action.type`相匹配.如果是简单的reducer,可能没有必要使用`switch`,理论上使用`if/else`可能更快一点.
如果`action.type`一点匹配,然后会返回和`action`相关的属性.和前面提到的一样,`type`和`action[属性名]`是在action creators里定义的.
好啦,了解到这些内容,在`reducers/item.js`中创建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根据结果store的state属性来命名,`action.type`没有必要想对应.前两个表达完整的意思,第三个`items()`就稍微有点不同.
这是因为,可能会有很多条件返回`items`数组:有可能返回所有的数组,有可能在删除dispatch以后返回`items`的次级结构.或者所有的items都被删除了,会返回一个空数组.
为了重新遍历,每一个reducer都会返回一个截然不同的state属性,不需要考虑reducer内部的条件到底有多少.刚开始花了我很长时间想明白这个问题.
单个的reducers创建好了以后,我们需要把单个的reducer合并(combine)成一个`rootReducer`,创建单一对象.
创建文件`reducers/index.js`
import { combineReducers } from 'redux';
import { items, itemsHasErrored, itemsIsLoading } from './items';
//由于每个reducer返回的都是一个对象
//所以这里的操作就是合并对象的操作,在underscore和loadsh
//里面可以找到合并js对象的代码
export default combineReducers({
items,
itemsHasErrored,
itemsIsLoading
});
我们从`items`里导入每个reducers,使用redux的`combineReducers()`函数来合并输出单一对象(译注:所以每一个reducer返回的对象的属性名应该是唯一的,否则就覆盖了,前面的内容表达过这个意思)
因为我们的reducer的名字和在store中使用的属性名一样,所以我们可以使用ES6的对象字面量.
注意,我有意提到了reducer的前缀,所以当我们的application变得比较复杂的时候,不能出现全局性的`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的时候使用别名.但是我更愿意使用独一无二的名字.
## 配置store,注入到你的app中
操作很直接,创建`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)
);
}
现在在index.js中包含` `组件,`configureStore`,配置`store`.包装app(` `),传递进去`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(
document.getElementById('app')
);
我知道,其实花了很多努力才到了这一步,但是随着设置的完成,我们就可以使用配置来操纵我们的组件了(译注:这里是意译,组件又称为木偶组件,意思很清楚吧?谁是拿着吊线的人呢?就是redux).
## 把组件转化为使用Redux store和方法
跳回到`components/ItemList.js`
在顶部导入需要的部分
import { connect } from 'react-redux';
import { itemsFetchData } from '../actions/items';
`connect`可以让组件链接到Redux的store,`itemsFetchData`是在开始写的action creator.我们仅仅需要导入actin creator.使用`dispatch`来触发actions.(译注:redux里面有很多内容其实是很基础的,例如这里,javascript的函数是一类对象,在js中函数是传引用的,所以函数名可以作为函数的引用,通过另一函数的参数来传递. 厉害 ).
在组件类定义好了以后,我们可以把Redux的state和action creator的dispatch映射到props上.
创建一个函数,接受`state`返回一个props对象.在简单的组件中,可以把前缀去掉.
const mapStateToProps = (state) => {
return {
items: state.items,
hasErrored: state.itemsHasErrored,
isLoading: state.itemsIsLoading
};
};
接着我们需要另一函数在props中`dispatch`我们的`itemsFetchData()`函数.
const mapDispatchToProps = (dispatch) => {
return {
fetchData: (url) => dispatch(itemsFetchData(url))
};
};
还是把`item`前缀去掉.这里的fetchData函数接受一个`url`参数,返回
派发调用`itemsFetchData(url)`操作.
现在,`mapStatetoProps()`和`mapDispatchToProps()`还不能做什么事情,需要改变一下代码
export default connect(mapStateToProps, mapDispatchToProps)(ItemList);
`connect`函数把组件连接到Redux,同时映射了需要用到的props.
最后一步是使用props替换掉state.
* 删掉`constructor(){}`和`fetchData(){}`
* 在`componentDidMount()`里把`this.fetchData()`改为`this.props.fetchData()`.
* `this.state.x` 统一改为`this.props.X`.
你的组件看起来应该是
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
Sorry! There was an error loading the items
;}
if (this.props.isLoading) {
return
Loading…
;}
return (
{item.label}
{this.props.items.map((item) => (
))}
);
}
}
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现在使用Redux和Redux thunk来fetch和显示数据.
也不是太困难,是不是?
你现在也是Redux大师了 :D
## 接下来做什么?
我在github提交了每一步的代码.我希望你克隆和理解他,然后添加根据items的id删除item的功能.
我没有真正提到Redux的内容是,state是immutable,意识是你不能修改他.所以在reducers中每次都要返回一个新的state,上面的三个reducers比较简单,刚刚好可以工作.但是如果是从items的数组中删除操作,你可能不熟悉.
你不能再使用`Array.protype.splice()`来从数组中移除items.因为这样做会mutate之前的state.[Dan在视频中讲解了怎么从数组中删除一个元素](https://egghead.io/lessons/javascript-redux-avoiding-array-mutations-with-concat-slice-and-spread).如果你坚持要做,可以看看`delete-items`分支,可以找到解决办法.
我真切的希望这篇文章讲明了Redux和Thunk的原理以及怎么把现有的React application转变到使用redux.写作过程巩固了我对Redux的理解(译注:翻译过程也巩固了我的理解),所以我很高兴这么做(译注:so do I ).
下面有点不翻了。
>我个人觉得这篇文章却似写的非常的好.强烈推荐.