该篇为翻译文章:原文为:Leveling Up with React: Redux

该系列文章目录:
第一部分:学习React Router-译文(原文)
第二部分:React的“容器组件”和“可视化组件”-译文(原文)
第三部分:React 之 Redux(当前)

Redux是一个管理SPA应用的数据状态和界面状态的工具。它不依赖于任何其它框架,也就是说它可以和React一起用,也可以和Angular一起用,也可以和jQuery一起用。

上一篇文章当中,我们了解了React的数据在组件间的流动性。更确切一点,其实它是“单向数据流”,因为它只会从父组件流到子组件。这样看来,如果是两个没有父子关系的组件之间想进行通信的话就出现问题了:

01

React不推荐非父子关系的组件进行直接通信,它根本就没提供这样的方法,因为没有关系的组件之间进行通信被认为是很糟糕的做法,最终的代码会变的非常烂。
React官方给出一个解决方案,那就是设置一个全局的事件管理系统,Flux设计模式就是这样来做的。

同样Redux也是这样处理的。Redux把应用的状态(state)统一到一个地方进行管理,叫做store。然后,组件把各自的状态分派(dispatch)到store当中去,并不是直接发送给其它组件。其它组件要做的就是需要订阅(subscribe) store当中的状态(state):

02

store可以被看做成一个“中间人”,它来管理整个应用的状态变化。所以,它和之前的解决方法的区别在于:

03

使用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,这个对象就是一个actionaction可以看做是一个”有效负荷”,因为它包含了状态的类型和一些状态数据,这些东西足够来改变状态。上面的代码当中,由type可以看出是要添加用户,而user则是具体的数据。记住,type属性是不能变的,而后面的属性是可以自定义的。

3.改变状态由一个纯函数来完成

我们已经知道,Redux不允许应用直接修改状态。而是通过发送一个带有状态信息的action来进行完成,那么这个action发送给谁呢?其实它发送给了ReducersReducers只是一些用来处理接收到的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'} });

让我们来看一下上面的代码都做了什么:

  1. 根据一个reducer创建了一个store
  2. reducer初始化将状态设置成了一个空数组
  3. 发送一个添加新用户的action请求
  4. reducer添加了一个新用户并将state返回,这样就更新了store当中的状态。

*在这个例子当中,reducer被调用了两次-一次是store被创建的时候,另一次是发送action的时候。
store被创建的时候,Redux立马调用了一下reducer进行了状态的初始化。第一次调用reducer时,state是undefined。在reducer函数里面,返回了一个空数组作为状态的初始化状态到store当中。
每当发送action的时候,Reducers就会被调用。而从reducer当中返回的新状态将被更新到store当中。
例子当中,当发送了action之后,Redux把当前状态(初始化的空数组)传给reducer。action中的typeADD_USER,reducer会根据这个来改变相应的状态。

你可以把reducer想象成一个漏斗,因为它总是接收状态,然后再返回更新后的状态:

04

例子中,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仍然是一个对象,但这个对象里面有嵌套的对象,userStatewidgetState里面可以包含任意类型的数据。
这时候,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的时候去改变它们各自的状态:

05

不可改变的数据

关于这点,我们之前说过一点,现在我们再提几点比较重要的。
首先,JS当中的原始数据类型(Number,String,Boolean,Undefined,Null)是不可改变的。而Objects,Arraysfunctions是可变的。
因为,容易变化的数据在数据结构中是容易产生bug的。由于存在store当中的数据大部分都是objectsarrays,所以,我们需要使用一些方法来让这些数据“不可变”。

假设,现在我们想改变一个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,reduxreact-reduxnpm当中三个不同的模块。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);

这里面发生了好多事?

  1. react-redux当中导入了connect方法。
  2. connect()方法可以接收两个参数,但例子当中,我们只传了一个参数进去。
  3. connect()的第一个参数是一个函数,这个函数应该返回一个对象。这个对象的属性以后将变成组件的props。可以看到,它们的值是来自state。mapStateToProps()函数默认接收到了store作为参数传入。其实mapStateToProps()的主要目的就是从store当中的state中提取它所需要的state。
  4. 因为有有第三条的存在,所以我们不再需要getInitialState()方法了。并且,我们用了this.props.users来代替this.state.users,因为现在用户这个数组是一个prop,而不仅仅是组件内容的一个state了。
  5. 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()方法的容器组件。

下面就是两个容器组件嵌套一个可视化组件的关系图:

06

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上获取,按照文档操作就可以有这样一个单页面应用:
07