该篇为翻译文章:原文为:Leveling Up with React: Redux
该系列文章目录:
第一部分:学习React Router-译文(原文)
第二部分:React的“容器组件”和“可视化组件”-译文(原文)
第三部分:React 之 Redux(当前)
Redux
是一个管理SPA应用的数据状态和界面状态的工具。它不依赖于任何其它框架,也就是说它可以和React
一起用,也可以和Angular
一起用,也可以和jQuery
一起用。
上一篇文章当中,我们了解了React的数据在组件间的流动性。更确切一点,其实它是“单向数据流”,因为它只会从父组件流到子组件。这样看来,如果是两个没有父子关系的组件之间想进行通信的话就出现问题了:
React不推荐非父子关系的组件进行直接通信,它根本就没提供这样的方法,因为没有关系的组件之间进行通信被认为是很糟糕的做法,最终的代码会变的非常烂。
React官方给出一个解决方案,那就是设置一个全局的事件管理系统,Flux设计模式就是这样来做的。
同样Redux也是这样处理的。Redux把应用的状态(state)统一到一个地方进行管理,叫做store
。然后,组件把各自的状态分派(dispatch)到store当中去,并不是直接发送给其它组件。其它组件要做的就是需要订阅(subscribe) store
当中的状态(state):
store
可以被看做成一个“中间人”,它来管理整个应用的状态变化。所以,它和之前的解决方法的区别在于:
使用Redux,所有的组件都可以很容易的从store
当中获取数据状态和向stroe
发送状态。组件的变化与否取决于组件是否向stroe
发送状态信息,而不用关心其它组件的变化。这就是Redux使数据流变的更流畅的原因。
还有一点需要说的是,使用store(s)
来管理组织应用状态这个模式是来源于Flux。这种设计模式非常适合用在像React这种单向数据流系统当中。那么React和Flux到底有什么区别呢?
Redux 是一个像 Flux的东西
首先,Flux是一种设计模式,并不是一个能下载的东西。而Redux是一个工具,是一个可以下载下来的东西,但是它来源于Flux。如果你之前用过Flux这种设计模式,你会发现它和Redux之间有一些小小的区别,特别是Redux的三个主要原则:
1.单个真实数据来源
Redux只用一个store
来存储管理整个应用的状态,所以叫做单个真实数据来源。
store
当中的数据结构取决于你自己,但是在真实的应用当中,一般都是一些很多层的对象。
而在Flux当中,则是使用多个store
来管理状态。
2.状态是只读的
Redux的官方文档是这样说的,“只有一种方法可以改变状态,那就是触发一个action
,action
是一个带有描述信息的对象”。
也就是说,应用不能直接修改状态。而是通过发送一个action
结果去改变store
当中的状态。
store
对象只提供了4个API:
store.dispatch(action)
store.subscribe(listener)
store.getState()
replaceReducer(nextReducer)
可以看到,没有一个方法是用来设置状态的。使用store.dispatch(action)
方法来发送一个action
是惟一的一种方法:
var action = {
type: 'ADD_USER',
user: {name: 'Dan'}
};
// Assuming a store object has been created already
store.dispatch(action);
dispatch()
发送了一个对象给Redux,这个对象就是一个action
。action
可以看做是一个”有效负荷”,因为它包含了状态的类型和一些状态数据,这些东西足够来改变状态。上面的代码当中,由type
可以看出是要添加用户,而user
则是具体的数据。记住,type
属性是不能变的,而后面的属性是可以自定义的。
3.改变状态由一个纯函数来完成
我们已经知道,Redux不允许应用直接修改状态。而是通过发送一个带有状态信息的action
来进行完成,那么这个action
发送给谁呢?其实它发送给了Reducers
,Reducers
只是一些用来处理接收到的action
的函数,它才是最终改变状态的东西。
一个reducer
默认接收了一个当前状态参数state
作为参数,它将返回一个新的state来改变state。
// Reducer Function
var someReducer = function(state, action) {
...
return state;
}
Reducers只是一个纯函数,但是它要有以下特性:
- 它不会直接发送一些网络请求,不会直接访问数据库。
- 它返回的值取决于它接收到的参数是什么样的。
- 它接收到的参数应该是不让被改变的。
之所以称之为纯函数,就是因为它只会根据传入的参数而返回一个值,除此之外,它不再做其它事了。
第一个Redux store
创建store是使用Redux.createStore()
方法,调用该方法的时候,需要把所有的Reducers
作为参数传进去。我们先传一个reducer来看一下:
// Note that using .push() in this way isn't the
// best approach. It's just the easiest to show
// for this example. We'll explain why in the next section.
// The Reducer Function
var userReducer = function(state, action) {
if (state === undefined) {
state = [];
}
if (action.type === 'ADD_USER') {
state.push(action.user);
}
return state;
}
// Create a store by passing in the reducer
var store = Redux.createStore(userReducer);
// Dispatch our first action to express an intent to change the state
store.dispatch({
type: 'ADD_USER',
user: {name: 'Dan'}
});
让我们来看一下上面的代码都做了什么:
- 根据一个
reducer
创建了一个store
reducer
初始化将状态设置成了一个空数组- 发送一个添加新用户的
action
请求 reducer
添加了一个新用户并将state返回,这样就更新了store当中的状态。
*在这个例子当中,reducer
被调用了两次-一次是store
被创建的时候,另一次是发送action
的时候。
当store
被创建的时候,Redux立马调用了一下reducer进行了状态的初始化。第一次调用reducer时,state是undefined
。在reducer函数里面,返回了一个空数组作为状态的初始化状态到store
当中。
每当发送action
的时候,Reducers就会被调用。而从reducer当中返回的新状态将被更新到store
当中。
例子当中,当发送了action
之后,Redux把当前状态(初始化的空数组)传给reducer。action
中的type
为ADD_USER
,reducer会根据这个来改变相应的状态。
你可以把reducer想象成一个漏斗,因为它总是接收状态,然后再返回更新后的状态:
例子中,store
当中应该有一个存储一个用户的数组:
store.getState(); // => [{name: 'Dan'}]
不要直接改变状态,而是拷贝一份
在我们的例子当中,reducer是可以正常工作的,但是它直接改变了state本身,这是一种不好的习惯。所以,我们最好不要用像
.push()
这样的会改变原数据的方法来改变传到reducer当中的state。
传到reducer中的数据应该是不希望被改变的。换句话说,它们不应该被直接改变,而是使用一些不会改变原数据的方法,像.concat()
,该方法会不改变原来的数组而是拷贝原数组,然后把这个拷贝出来的数组返回:
var userReducer = function(state = [], action) {
if (action.type === 'ADD_USER') {
var newState = state.concat([action.user]);
return newState;
}
return state;
}
多个Reducer
之前的例子是一个很好的入门,但是在真正的应用当中,会有很多很复杂的状态。因为Redux只有一个store
,所以我们需要将这些状态使用对象的嵌套来进行管理。现在,假设我们的store是这样的:
{
userState: { ... },
widgetState: { ... }
}
现在,store
仍然是一个对象,但这个对象里面有嵌套的对象,userState
和widgetState
里面可以包含任意类型的数据。
这时候,reducer也要根据store中不同的数据来进行创建了:
import { createStore, combineReducers } from 'redux';
// The User Reducer
const userReducer = function(state = {}, action) {
return state;
}
// The Widget Reducer
const widgetReducer = function(state = {}, action) {
return state;
}
// Combine Reducers
const reducers = combineReducers({
userState: userReducer,
widgetState: widgetReducer
});
const store = createStore(reducers);
combineReducers()
允许我们根据不同的reducer来描述store
,最终把store
中的状态和reducer进行对应起来。当每个reducer返回初始化状态时,该状态将会找到store
中对应的单元当中去。
现在,有一点是非常重要的:每一个reducer都通过store
整体的state找到它们各自的那部分state,而不是这个最外层的store
在发挥作用,然后从每个reducer当中返回的state应用到store各自的那部分去。
哪一个Reducer先被调用?
其实是所有的reducer同时被调用,把这些reducers串成漏斗的话就会更明显,所有的reducers都将会在各自接收到action
的时候去改变它们各自的状态:
不可改变的数据
关于这点,我们之前说过一点,现在我们再提几点比较重要的。
首先,JS当中的原始数据类型(Number
,String
,Boolean
,Undefined
,Null
)是不可改变的。而Objects
,Arrays
和functions
是可变的。
因为,容易变化的数据在数据结构中是容易产生bug的。由于存在store当中的数据大部分都是objects
和arrays
,所以,我们需要使用一些方法来让这些数据“不可变”。
假设,现在我们想改变一个state,它是一个对象,我们有三种方法:
// Example One
state.foo = '123';
// Example Two
Object.assign(state, { foo: 123 });
// Example Three
var newState = Object.assign({}, state, { foo: 123 });
其中,第一种和第二种方法改变了state对象。第二种方法把参数都合并到了state上。只有第三种方法是比较适合的,它把参数和state合并到了一个全新的对象上面。
ES6中的“展开运算符”是另一种很好用的方法:
const newState = { ...state, foo: 123 };
Redux 和 React
我们已经说过,Redux并不依赖任何其它框架,它可以和任何其它框架结合使用。现在,我们要将它和React当中的容器组件结合到一起。
首先,这是一个普通的React组件:
import React from 'react';
import axios from 'axios';
import UserList from '../views/list-user';
const UserListContainer = React.createClass({
getInitialState: function() {
return {
users: []
};
},
componentDidMount: function() {
axios.get('/path/to/user-api').then(response => {
this.setState({users: response.data});
});
},
render: function() {
return <UserList users={this.state.users} />;
}
});
export default UserListContainer;
组件里进行了Ajax的请求,并更新了自己的state。但是如果其它地方需要用到用户列表的话,那么这种方法很明显是行不通的。
使用Redux,我们可以发送一个action
来代替上面的this.setState()
方法。然后,这个组件和其它组件可以订阅这个状态的变化。但问题是,我们该如何设置store.subscribe()
来更新组件的状态呢?
接下来就需要另一个模块react-redux
来完成这个工作了。
使用react-redux
进行连接
react
,redux
和react-redux
是npm
当中三个不同的模块。react-redux
模块可以将我们的React组件和Redux连接起来。
使用react-redux
后应该是这样的:
import React from 'react';
import { connect } from 'react-redux';
import store from '../path/to/store';
import axios from 'axios';
import UserList from '../views/list-user';
const UserListContainer = React.createClass({
componentDidMount: function() {
axios.get('/path/to/user-api').then(response => {
store.dispatch({
type: 'USER_LIST_SUCCESS',
users: response.data
});
});
},
render: function() {
return <UserList users={this.props.users} />;
}
});
const mapStateToProps = function(store) {
return {
users: store.userState.users
};
}
export default connect(mapStateToProps)(UserListContainer);
这里面发生了好多事?
- 从
react-redux
当中导入了connect
方法。 connect()
方法可以接收两个参数,但例子当中,我们只传了一个参数进去。connect()
的第一个参数是一个函数,这个函数应该返回一个对象。这个对象的属性以后将变成组件的props
。可以看到,它们的值是来自state。mapStateToProps()
函数默认接收到了store
作为参数传入。其实mapStateToProps()
的主要目的就是从store
当中的state中提取它所需要的state。- 因为有有第三条的存在,所以我们不再需要
getInitialState()
方法了。并且,我们用了this.props.users
来代替this.state.users
,因为现在用户这个数组是一个prop,而不仅仅是组件内容的一个state了。 - Ajax请求成功后改成了发送一个
action
,而不是以前的直接修改组件状态。
注意到上面的代码中,store
有一个userState
属性,但是这个属性名是从哪来的呢?
const mapStateToProps = function(store) {
return {
users: store.userState.users
};
}
其实这个userState
来自于我们绑定的reducer:
const reducers = combineReducers({
userState: userReducer,
widgetState: widgetReducer
});
那么userState
的.user
属性又是从哪来的呢?
我们并没有在代码当中展示reducer(因为它应该在被写在另外一个文件当中),如果把reducer的代码放出来,大家就会知道.user
是从哪来的了:
const initialUserState = {
users: []
}
const userReducer = function(state = initialUserState, action) {
switch(action.type) {
case 'USER_LIST_SUCCESS':
return Object.assign({}, state, { users: action.users });
}
return state;
}
Ajax当中发送action
例子中,我们只发送了一个action
。就是USER_LIST_SUCCESS
。我们还想在Ajax请求之前发送一个USER_LIST_REQUEST
,Ajax请求失败再发送一个USER_LIST_FAILED
。具体做法可以参考 异步Actions
在事件中发送action
上一篇文章当中,我们知道了事件函数应该从容器组件传送到可视化组件当中。其实,我们可以使用react-redux
来让事件发送action
:
...
const mapDispatchToProps = function(dispatch, ownProps) {
return {
toggleActive: function() {
dispatch({ ... });
}
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(UserListContainer);
在可视化组件当中,我们可以像以前一样使用onClick={this.props.toggleActive}
。
容器组件的疏漏
有时候,容器组件只需要订阅store
中的状态,不需要任何其它的方法,比如一个componentDidMount()
方法来发送Ajax请求。也可能只需要一个render()
方法来把state传给子组件。针对这种情况,我们可以用这种方法来创建容器组件:
import React from 'react';
import { connect } from 'react-redux';
import UserList from '../views/list-user';
const mapStateToProps = function(store) {
return {
users: store.userState.users
};
}
export default connect(mapStateToProps)(UserList);
嗯?组件呢?为什么创建组件的方法没有了?
其实,connect()
方法已经相当于为我们创建了一个容器组件。注意,我们这次是直接把可视化组件作为参数传入的。
如果说自动创建了一个容器组件,那么之前写的例子是不是相当于有两个容器组件进行了嵌套了呢?答案是肯定的,但这不是一个问题。上面这种写法只适用于只需要一个render()
方法的容器组件。
下面就是两个容器组件嵌套一个可视化组件的关系图:
Provider
为了使前面说过的这些react-redux
代码起作用,你需要使用<provider></provider>
组件。这个组件需要包含你的整个应用,如果你用React Router的话,那么代码可能会是这样的:
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import router from './router';
ReactDOM.render(
<Provider store={store}>{router}</Provider>,
document.getElementById('root')
);
store
被绑定到了Provider
组件上,这个代码所在的文件应该是应用的主入口文件。
Redux 和 React Router
它们并不是一定要有联系,但是有一个npm
的项目叫做react-router-redux。从技术角度来说,路由是UI状态的一部分,所以React Router并不知道Redux做的一些工作和它里面的东西,而react-router-redux
则可以把它们连接起来。
最终项目
最终的项目只可以从GitHub上获取,按照文档操作就可以有这样一个单页面应用: