Redux使用教程

本教程假设你有React 和 ES6/2015经验。首先从没有使用Redux最简单情况开始,演示到使用Redux从无到有的变化过程,从对比中体会Redux好处。

首先创建一个React组件components/ItemList.js用来抓取和显示条目列表。设置这个静态组件带有状态包含各种items输出,2个boolean状态表示在抓取加载时正常和出错的结果。


import React, { Component } from 'react';
class ItemList extends Component {
constructor() {
super();
this.state = {
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) {
return <p>Sorry! There was an error loading the items</p>;
}
if (this.state.isLoading) {
return <p>Loading…</p>;
}
return (
<ul>
{this.state.items.map((item) => (
<li key={item.id}>
{item.label}
</li>
))}
</ul>
);
}
}
export default ItemList;

当渲染时,这个组件应该输出4个条目列表,但是如果设置 isLoading或hasErrored为true时,也会有相应状态输出。

假设我们从 JSON API抓取items,使用Fetch API实现异步请求,它比传统的XMLHttpRequest更方便,能够返回一个响应的promise,Fetch并不适合所有浏览器,需要加入你项目依赖:
npm install whatwg-fetch --save

抓取逻辑很简单:
1.首先,设置初始化items为空数组[]
2.加入抓取方法,设置loading和错误信息


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 })) // ES6 property value shorthand for { items: items }
.catch(() => this.setState({ hasErrored: 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() {
}
}

现在,我们已经有一个没有使用redux普通的抓取组件,从一个REST端点抓取条目列表,在条目全部抓取出现之前,显示loading。。状态,而如果发生错误,可以显示错误。

无论如何,一个组件不应该包含抓取数据的逻辑,数据也不应该保存在组件的状态中,要解决这两个问题,就需要redux。

下面我们将上面案例代码转换为redux,我们需要加入Redux,为了在Redux中实现异步调用,我们还需要Redux Thunk

什么是Thunk?Thunk是一个函数,用于包装一个表达式以延迟其计算结果的获得,看下面代码:


// 立即计算 1 + 2
// x === 3
let x = 1 + 2;

// 1 + 2计算被延迟
// foo 能在以后被调用时再进行计算
// foo 就是一个 thunk!
let foo = () => 1 + 2;

下面是在我们当前案例安装Redux, React Redux和Redux Thunk:
npm install redux react-redux redux-thunk --save

理解Redux
Redux有一些核心原理我们需要理解:
1.有一个全局状态对象管理整个应用的状态,在这个案例中,它是类似于我们这个最初组件的状态,这属于真相单一来源。

2.只有一个办法才能修改状态,只能通过一个动作action,这是一个用来表示改变操作的对象,Action Creators是专门用来派发每个改变的函数,所做的就是返回一个动作action对象。

3.当一个动作action被派发时,一个Reducer函数将实际改变这个动作所要改变的状态,如果动作不适合这个Reducer,就返回已经存在状态。

4.Reducer时纯函数,它们不会有副作用,没有可变状态,它们都必须返回一个修改的复制,

5.独立reducer被结合到单个rootReducer上,以创建状态的各种离散状态。

6.Store是将上面所有东西都结合一起,通过使用rootReducer代表状态,允许你真正dispatch动作action

7.对于在React中使用Redux,<Provider />组件包装整个应用,传递store下传到所有子成员。

设计状态
从开始工作到现在,我们已经明白我们状态需要有三个属性: items,hasErrored,isLoading,对应三个动作action。

但是Action Creators不同于动作action,不需要与状态有1:1关系,我们需要第四个action creator来调用其他三个action creators,这取决于抓取数据的状态,这第四个action creator几乎等同于我们这个案例原始fetchData()方法,但是不是使用this.setState({ isLoading: true }) 方法来直接修改状态,我们通过派发dispatch一个动作action来做同样事情:dispatch(isLoading(true))

创建动作action
让我们创建actions/items.js文件来放我们的action creators,我们首先开始3个简单动作:


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会返回一个动作action,我们使用export以便以后在其它代码地方使用它们。

第二个action create获取bool(true/false)作为参数,返回带有意义的一个type和一个分配有具体相应属性的bool这两个类型的对象。

第三个itemsFetchSuccess是当数据被成功抓取时调用,将数据作为items传递,通过ES6的value shorthands魔术,我们返回一个对象,带有属性items,它的值将是items的数组。

现在我们有3个动作action代表我们的状态,我们将转换原来组件的fetchData方法为itemsFetchData()这样新的action creator。

默认情况下,Redux的action creators不会支持异步动作,比如异步抓取数据,这里就需要使用Redux Thunk,它能让你编写action creator返回一个函数而不是一个动作action对象,内部函数能够接受store方法dispatch和getState作为参数,但是我们只使用dispatch。

如果不使用thunk,实现异步的最简单方法是5秒后手工触发itemsHasErrored方法:


export function errorAfterFiveSeconds() {
// We return a function instead of an action object
return (dispatch) => {
setTimeout(() => {
// This function is able to dispatch other action creators
dispatch(itemsHasErrored(true));
}, 5000);
};
}

现在有了Thunk,我们编写itemsFetchData方法:

export function itemsFetchData(url) {
return (dispatch) => {
dispatch(itemsIsLoading(true));
fetch(url)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
}
dispatch(itemsIsLoading(false));
return response;
})
.then((response) => response.json())
.then((items) => dispatch(itemsFetchDataSuccess(items)))
.catch(() => dispatch(itemsHasErrored(true)));
};
}

待续。。。
A Dummy’s Guide to Redux and Thunk in React – Medi
[该贴被tecentID39C3D于2016-11-18 13:16修改过]

创建reducer
前面已经定义了action creators,我们现在编写reducer,用来获取这些action creators,然后返回应用新的状态。

每个reducer有两个参数:之前状态和action对象,我们也使用ES6的默认参数特性来设置默认初始状态。

在每个reducer中,我们使用switch语句来判断什么时候action.type匹配,而在我们这个简单案例中好像没有必要,你的reducer能理论上有许多条件。

如果action.type匹配,那么我们返回action的相关属性,正如早前提到,type 和action[propertyName]是在你的action creator中定义。

好了,知道这些,让我们创建reducers/items.js:


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属性。为了重新遍历,每个reducer会返回一个状态离散属性,无论reducer中有多少条件。

当独立reducer创建后,我们需要结合它们进入一个rootReducer,创建新文件在reducers/index.js:


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

我们从items.js中导入每个reducer,使用Redux的combineReducers()来export输出它们,因为reducer的名称等同于我们用于store的属性名称,这里能使用ES6的shorthand特性。

如果随着系统增长,不只是有hasErrored和isLoading属性,可以灵活地加入:


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
});

配置store,将其提供到你的应用
创建文件在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来包含<Provider /> configureStore,设置我们的store并包装应用 (<ItemList />) 将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')
);

好不容易走到这里,最后我们终于修改组件来利用这些所做的事情了。

转换组件使用Redux store和方法
跳回components/ItemList.js
在顶部导入import我们需要的:
import { connect } from 'react-redux';
import { itemsFetchData } from '../actions/items';

connect能让我们连接一个组件到Redux的store,而itemsFetchData是action creator,前面已经编写了,只需要导入。

在我们的组件class定义,我们会map Redux的状态和action creaor的dispatching到属性

我们创建一个函数接受state然后返回属性的一个对象,在这个简单案例中,移除前缀has/is属性,因为它们很明显与items相关。


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

需要另外一个函数能够使用dispatch我们的itemsFetchData() action creator:


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

这里fetchData是一个函数,接受url作为参数,返回itemsFetchData(url)的disptach。

现在有了两个mapStateToProps() 和 mapDispatchToProps(),将它们export即可:
export default connect(mapStateToProps, mapDispatchToProps)(ItemList);

这个connect是连接ItemList到Redux,当map我们的这两个属性props使用之时。

最后一步转换组件使用props而不是state:
1. 移除constructor() {} 和 fetchData() {},因为不需要了
2.改变componentDidMount()中的this.fetchData()到this.props.fetchData()
3.改变this.state.X 到.hasErrored, .isLoading 和 .items的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 <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);

全文完成,代码见:Github