【翻译】在 react 中使用 redux
原文地址: http://codepen.io/stowball/post/a-dummy-s-guide-to-redux-and-thunk-in-react
如果你像我一样,在读了 redux 文档、看过了 Dan’s 教学视频、完成了 We’s 课程,还是没有搞明白如何使用 redux,那么希望此篇文章能够帮到你。
在我试用过好几次的 redux 后才理解了 redux 的用法,将一个从 ajax 接口接收 json 数据的应用改变成使用 redux/redux thunk 的模式。 因此我觉得应该将此过程记录下来,如果你不懂什么是 thunk,那也别着急,我们将在 “Redux way” 的异步调用中使用到它。
此篇文章假设你已经有了 react 和 ES6 的基础,掌握这些应该也是比较简单的。
非 Redux 模式
我们先来写一个 react component:components/ItemList.js
,调用一个接口获取列表数据。
基础内容
首先,我们在 state
中设置一些静态的数据:包括主要显示内容的 items
,还有两个 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;
这看起来不太像是一个非常漂亮的开局,但至少是算是好的。
当渲染完毕后,component 会展示出这4个列表元素,如果你将 isLoading
或 hasErrored
置成 true,一个对应的 <p>
会替代列表显示 。
改成动态组件
把这些元素都写死的话,这个 component 肯定没法用的,so 我们需要从一个 json api 中获取 items
数据,同时也需要我们在适当的时候将 isLoading
或 hasErrored
正确的置位。
接口提供的 items
数据和我们造的是完全一致的,当然在真实情况下,它可能是一个畅销书的列表、一个最新的博文列表,或是其它符合你业务场景的数据。
获取数据我们用 fetch,fetch
使用起来比传统的 XMLHttpRequest
更方便,而且返回的 response 结果是一个标准的 promise
对象(这对于 Thunk 模式非常重要)。
fetch
无法在所有浏览器中得到支持,所以你需要在项目中安装以下组件:
npm install whatwg-fetch --save
开始转换这一部分,非常简单:
- 将初始的
items
置成一个空的数组; - 添加获取数据的方法和控制
isLoading
和hasErrored
的状态:
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() {
}
}
你的组件通过一个 REST
端获取数据,我们希望在items
获取之前能够看到一个简单的 loading...
。
如果接口获取失败,也应该有一个相应的错误提示。
然而,在现实情况下,一个 component
不应当包括抓取数据的逻辑,数据也不应当存储在 component
的 state
中,
这个时候, Redux 就应声而来了。
转换成 Redux 模式
理解 Redux
在这之前,我们需要理解几个 Redux 的核心原则:
- 有一个全局的
state
来控制整个应用的各个state
,在这个例子中,它的作用和我们初始化 state 的行为是一致的,就是所谓的‘真相只有一个’; - 唯一改变 state 的方法就是通过引入的 action,它是一个描述哪个数据应当被修改的对象(方法)。Action Creators 是一些
function
,它们被用来发起这些改变,所作的就是返回一个 action; - 当一个 action 被发起,reducer 就上场了,它会根据相关的 action 改变真实的 state 属性,或是当这个 action 不适用与这个 reducer 时,返回当前的 state;
- reducer 应该是一个“纯函数”,不应当对传入的 state 产生任副作用,或是改变 state 本体,它应该返回一个修改后的副本;
- 单个的 reducer 被合并成一个 rootReducer,从页产生出那些互不相关的 state 属性;
- Store 的作用是将这些整合到一块去,它通过
rootReducer
方法和任何中间件来表示状态(在本例中为Thunk),并允许您dispatch
action; - 在 React 中使用 Redux,
<Provider />
包裹了整个的应用,并且将store
向下传递给在子结点;
随着我们将项目转换成使用 redux
的模式,这些原则也将变得更加清晰。
设计我们的 state
从我们已经完成的那些内容来看,认识到我们的 state 需要3个属性:items
, hasErrored
和 isLoading
,这样它就能在我们所预期的所有情况下工作了,因此,我们也需要三个相关的 action。
到这里,需要说明为什么 Action Creators 和 Actions 不一样,而且也不是 1对1 的关系:我们需要第四个 actions Creator,来根据我们所获取到的数据结果调用其它3个不同的 action。
这第4个 action creator 基本上和我们原始的 fetchData()
方法作用是一致的,但是它不会直接显式的调用 this.setState({ isLoading: true })
来改变组件的 state,而是 dispatch
一个 action 来作这件事:dispatch(isLoading(true))
。
编写 action
我们来写一个 actions/items.js
,作为 action creators,我们从三个简单的方法开始:
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 creator 返回的是 action。 我们将每个方法 export,这样就可以代码库任何地方来使用它。
前两个 action creators 将 boolean 值作为参数,同时返回一个对象,对象包括一个特定字符串的 type,和对应属性的 bool 值。
第三个方法 itemsFetchSuccess()
将会在数据成功获取后调用,同时将所需的数据作为 items
进行传递,
利用 es6 “property shorthands” 的特性,我们可以将 items
赋值给名称同为 items
的属性。
注意:你所用到的 type 的值和你需要返回的 property 属性名是非常重要的,因为你还要在 reducer 中用到它。
现在,我们已经有了3个 action 来表示所谓的 state, 现在我们来修改原有的 fetchData
方法,转换成 itemsFetchData()
action。
默认情况下,Redux action creators 不支持像 fetch
这样的异步数据,因此,在这里我们需要使用 redux thunk 模式。
Thunk 允许你编写一个返回 function 的 action creators,而不是返回一个对象。
这个内部函数可以接收 store 方法中的 dispatch 和 getState 作为参数,但是我们只使用 dispatch。
举个简单的例子,比如是在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)));
};
}
编写 reducers
action creators 已经搞定,现在我们来写 reducers,用来调用 actions 并且给应用返回一个新的 state。
注意:在 Redux 中,每个 redux 被调用时不会管你其中的 actions 写的是什么,所以如果某个其中的 action 并没有被应用到, 你也必须返回原始的 state。
每一个 reducers 都包括两个参数:修改前的 state
和 action
对象,你也可以用 es6 参数默认值的特性来给 state 初始化默认值。
在每一个 reducer 中,我们用 switch 语法来匹配某一个 action.type
,虽然在一些简单的 reducer 中这些看起来并没有什么必要,
但是你的 reducer 在理论上肯定会有很多条件,用 if/else
的话,就显的很淩乱了。
如果某一个 action.type
被匹配到,我们接下来就要返回这个 action 相关的属性,
就像文章早些时候提到的,type
和 action[propertyName]
被定义在你的 action creator
中。
了解这些后,我们来搞一个 reducer :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 属性处理结果来命名的。action.type
不一定是完全对应的,前两个 reducer 的命名是完全有意义的,最后一个 items
就不太相符了。
这是因为它可能在多个条件下返回的都是 items
数组:可能是在正常获取成功的情况下返回全部;可能是删除操作后返回一个子集,或者是列表被全删除后返回一个空列表。
需要重申一下,不管 reducer 传入了多少的条件,每一个 reducer 返回的是 state 的离散属性。这个原则在刚开始还让我纠结了一会儿。
这个单个的 reducer 创建后,我们需要一个 rootReducer 将它们联合起来,产生一个单独的对象。
reducers/index.js
如下:
import { combineReducers } from 'redux';
import { items, itemsHasErrored, itemsIsLoading } from './items';
export default combineReducers({
items,
itemsHasErrored,
itemsIsLoading
});
我们从 items.js
中引入每一个 reducers,再通过 Redux’s combineReducers()
进行统一 export
。
在这里,reducer (export) 的名称和 store
的属性名一致,也可以用 es6 的 shorthand 特性。
在这里注意一下,我是如何特意规定 reducer 的命名前缀的。 当你的应用复杂性不断增加时,也不会受到如 “hasErrored”、“isLoading” 等全局属性的干扰,因为你很有可能有许多不同的功能会出现错误或正在加载的状态。 因此给每一个 reducer 增加业务前缀,这样使你的应用 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
});
还有一种方向是导入时用别名 as
,但笔者更喜欢代码的一致性,别名会增加代码的阅读难度。
配置 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)
);
}
调整一下 app’s index.js
,引入 Provider
、configureStore
,装载我们写好的 store
,并在 <ItemList />
外面包裹一层 <Provider />
,如此将 store
作为 props
传入 app。
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 成功加载完毕后,我们就可以修改组件来满足
用 Redux 的 store 和 方法 来改写 component
我们先跳到 components/ItemList.js
。
在文件的顶部,引入我们所需的模块:
import { connect } from 'react-redux';
import { itemsFetchData } from '../actions/items';
connect
用来联接 component 和 redux 的 store,itemsFetchData
是之前写的 action。
我们目前只需要引入这一个 action creator,它负责处理调用其它 action。
定义好 component 类,我们来做一个 Redux’s state 和 action 调用方法的映射,用来将两者对应到组件的 props 里。
定义一个方法 mapStateToProps
,返回 props 对象。在上面那个简单的 component 里,我去掉了 has/is
前面的业务前缀,
因为在这里,很明显它就是和 items
关联的。
const mapStateToProps = (state) => {
return {
items: state.items,
hasErrored: state.itemsHasErrored,
isLoading: state.itemsIsLoading
};
};
接着我们需要另外一个方法来通过 props dispatch
之前写的 itemsFetchData()
action。
const mapDispatchToProps = (dispatch) => {
return {
fetchData: (url) => dispatch(itemsFetchData(url))
};
};
我们在返回对象的属性中去掉了 items
的前缀。
fetchData
是被定义成一个接收 url
参数然后返回一个 dispatch
调用 itemsFetchData(url)
的方法。
目前,mapStateToProps()
、mapDispatchToProps()
这两个方法还没有做任何事情,我们需要改变最终的 export
如下:
export default connect(mapStateToProps, mapDispatchToProps)(ItemList);
此处 connect
方法通过 props
映射的方式将 ItemList
组件接入 Redux,供我们在 view 组件中使用。
最后一步,将组件从使用 state
转换成使用 props
,然后按些去掉多余的东西:
- 删掉
constructor() {}
和fetchData() {}
方法,现在它们已经不需要了。 - 将
componentDidMount()
中的this.fetchData()
改成this.props.fetchData()
。 - 将
.hasErrored
、.isLoading
和.items
对应的 state 值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 <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);
这就是所谓的用 Redux 和 Redux Thunk 来抓取和展示数据的 react 应用。
也不是很难嘛~
后话
笔者已将上述例子放置到 github,每一步都有 commit。 你可以 clone 下来,运行后理解一下,再试着添加一个用户根据不同项目的索引,删除列表中某个列表元素的功能。
之前我没有提到过,在这里我需要说一下,redux 中的 state 是不可变的,你不应该去修改它,只能是通过 reducer 来返回一个新的 state。 我们之前写得3个 reducer 是非常简单的,仅作演示。但是,如果要在一个数组中按索引删除一个元素,就需要和以上不同的方法了。
此类情况不能使用 splice
来从数组中删除元素,这样会改变原始数组。
Dan 展示了如何从数组中删除一个元素 如果你有疑问,可以 checkout 出 delete-items 分支看一眼。
我希望通过这篇博文能够解释一下 redux 和 thunk 的概念,通过教你怎样将 react 应用转换成使用 redux(thunk) 模式。 当然,我也通过这篇文章巩固了我对此的理解,本人也是很高兴的。
最后还是建议你阅读 redux 文档、看一下 Dan 的视频 和 Wes 的教程,通这些来了解 redux 更深一层的原理。