从零开始的react入门教程(十),快速上手react-redux,相对于redux它究竟简化了什么?
壹 ❀ 引
在前面两篇文章中,我们介绍了redux
与context
部分概念与基本用法,这里我们做个简单复习。
redux
属于应用数据流框架,主要用于应用状态的管理,比如react
中的state
。其数据流为view-->action-->reducer-->store-->view
,比如用户点击了一个按钮,本质触发的是store.dispatch(action)
,然后reducer
感知事件,触发actionType
对应的更新数据方法,从而达到更新store
的目的,而当store
更新后,早在view
订阅的store.subscribe
又被触发,这样又通过store.getState
拿到最新的store
并将其设置为组件的state
,state
发生变化自然会让组件重新render
,这便是一次完整的数据更新过程。
而随之问题也暴露出来了,多个组件需要使用store
的数据都得引入store.js
文件,而且dispatch
以及subscribe
等API都在store
上,所以你需要使用这些方法的地方也一样得提前引入store
。于是,我们紧接着介绍了context
概念,context
(上下文)的作用主要用于解决组件跨级传值问题,比如上面提到的API方法,我们就可以通过context.Provider
的value
将store
传递下去,这样不管哪个组件都可以通过类似this.context.dispatch
的写法调用store
上的方法。除了全局传递外,比如A-->B-->C-->D
场景,D需要拿到A的数据,但是又不希望BC作为数据传递的工具人,使用context.Consumer
同样能解决这一类场景的问题。
接下来要介绍的react-redux
是redux
作者为react
量身定制的库,使用上进一步简化了我们对于redux
以及context
的写法。由于react-redux
是独立redux
的存在,因此我们需要单独引入它。在项目根目录执行npm install --save react-redux
,接下来我们来了解其在用法的变化,没关系,有前两篇文章的铺垫,这并不会很难!本文开始。
贰 ❀ react-redux
react-redux
提供了Provider
,connect
,mapStateToProps
与mapDispatchToProps
四个API,我们来一一介绍它们。
贰 ❀ 壹 Provider
在之前context
中我们使用Provider
得先通过React.createContext()
创建,不过有了react-redux
后我们可以直接引用,比如:
import { Provider } from 'react-redux'
其用法与含义与之前完全相同,我们还是使用之前文章的例子,在index中做部分修改,将Provider
的引用改为react-redux
:
import React, { Component } from 'react';
import { Provider } from 'react-redux'
import ReactDOM from 'react-dom';
import store from './Store.js';
import Counter from './Counter.js';
import Summary from './Summary.js';
class ControlPanel extends Component {
render() {
return (
<div>
<Counter caption="First" />
<Counter caption="Second" />
<hr />
<Summary />
</div>
);
}
}
ReactDOM.render(
<Provider value={store}>
<ControlPanel />
</Provider>,
document.getElementById('root')
);
注意,这次我们把Provider
用在了ReactDOM.render
中,也就是进行了真正意义上的全局包裹,之前文章的例子由于ControlPanel
组件自身没有使用到store
的数据,而是它的子组件需要用,所以之前的写法如下,也没有什么问题:
class ControlPanel extends Component {
render() {
return (
//我们使用了Provider包裹子组件,通过value传递store
<context.Provider value={store}>
<div>
<Counter caption="First" />
<Counter caption="Second" />
<hr />
<Summary />
</div>
</context.Provider>
);
}
}
ReactDOM.render(
<ControlPanel />,
document.getElementById('root')
);
这里只是做个写法纠正说明,并不是react-redux
特性如此必须这么写,所以单独做个说明。保存后运行项目会报错,毕竟后续还有代码还没改完,我们先这样。
贰 ❀ 贰 connect
上面的代码修改,我们为全局上下文中添加了数据store
,还记得在之前context
介绍中如何使用上下文中的数据吗?两种方式,第一种是通过conponentName.contextType = context
先为当前组件绑定上下文,之后在constructor
中通过super(context)
引入,之后就可以通过this.context
访问上下文了,第二种方式通过Consumer
中回调函数形参直接访问。
而在react-redux
中我们通过connect
方法帮助组件链接全局上下文,比如:
import { connect } from 'react-redux';
export default connect(mapStateToProps, mapDispatchToProps)(component);
上面代码中component
就是你当前创建的组件,由于这个组件没有与上下文扯上关系,所以这里我们使用connect
帮助它链接全局store
,其次connect
接受两个参数,分别是mapStateToProps
与mapDispatchToProps
,有什么用我们后面再介绍。
这里我们补充一个概念,react-redux
将组件分为UI组件
与容器组件
。UI
组件很好理解,只负责view
渲染,不管理state
变化,不管理业务逻辑,也不使用redux
的API,它所接受的一切数据方法均由props
提供。
而所谓容器组件
作用与UI组件
互补,它的工作是负责state
变更以及业务逻辑处理,而这里的容器组件其实由react-redux
生成。
注意上面connect
这行代码,它本质上等同于:
// 帮UI组件注入数据方法,于是得到了一个新的容器组件,connect就像给一部手机通了电一样,让其有了生命力
const 容器组件 = connect(mapStateToProps, mapDispatchToProps)(UI组件);
export 容器组件;
也就是说我们通过connect
帮助一个UI
组件链接到了数据层,这里的数据可以是全局的store
,也可以是上层组件传递下来的props
。于是我们得到了一个被注入了数据的新组件。
说到这里,不知道大家能不能感受到这种做法与context
使用全局store
的差异性,context
使用数据要么直接把上下文与组件绑定后使用,要么借用Consumer
使用,就像被内嵌进组件一样,耦合度较高。而connect
更像在组件外部给其开了一个传递数据的入口,不管你数据哪里来的,都通过我这里传递进入,方式上就比较统一了,无论是全局store
还是上次的props
,都可以通过这里以props
的方式统一注入,那么组件内部呢都给我通过props
的方式访问外部传递进来的方法或者数据。
当然,上述关于UI组件
的分类说明比较严格了,实际项目开发中也存在很多包含了业务代码以及自身state
的组件使用connect
链接store
的做法。这里只是科普UI组件
与容器组件
的概念,有时候硬要将一个组件抽离成两个组件反而是一件麻烦事,具体看大家习惯。
上面说了connect
用于帮助组件链接数据,那么具体做呢?其实就得依赖上面提到的两个参数了,我们接着说。
贰 ❀ 叁 mapStateToProps
顾名思义,建立state
与props
的映射,mapStateToProps
接受2个参数,比如:
const mapStateToProps = (state,ownProps) => {
return {
name:state.name,
age:ownProps.age
}
}
说直白点,比如从全局的store
(store
本身可以理解为全局的state
)以及外层组件传递给你的props
中提取并组合成当前组件所需要的数据。
上面的代码中返回了一个新的对象,这个对象包含了一个name
一个age
,数据来源分别是全局state
与外层组件传递的props
,那么当前组件通过this.props
即可访问到这两个属性,而且,无论是外部的state
还是外部传递的ownProps
发生了变化,都会再次触发此方法(如果你没用这两个参数就不会触发,毕竟没有依赖关系),目的是同步更新传递当前组件的props
。
贰 ❀ 肆 mapDispatchToProps
看到方法名中的dispatch
,直觉应该想到此方法应该跟store.dispatch
有关。没错,我们在学习context
时,凡是组件需要派发action
时最终调用的都是this.context.dispatch
,写法上多少有些繁琐。mapDispatchToProps
的作用,其实也是将dispatch
行为抽离了出去,然后也作为props
一部分传进了组件,此方法接受两个参数,如下:
function mapDispatchToProps(dispatch, ownProps) {
return {
onIncrement: () => {
dispatch(Actions.increment(ownProps.caption));
},
}
}
dispatch
作用其实与store.dispatch
作用相同,只是react-redux
对齐进行了封装,我们不用再借用this.context.dispatch
调用,而是可以直接在mapDispatchToProps
方法中使用,是不是用法上便捷了很多呢?其次,mapDispatchToProps
返回了一个对象,包含了一个方法,那么在当前组件内部,调用this.props.onIncrement
本质上其实就是在做派发。
ownProps
与mapStateToProps
的第二个参数作用相同,都是外层组件组件传递的props
。
叁 ❀ 一个例子
OK,我们介绍了react-redux
带来的新概念,让我们使用这些API
改写上一篇文章中的例子,近距离感受它们所带来的便捷性。在前面我们其实已经完成了对index.js
的改写,接下来要做的是对Counter
组件与Summary
组件的改写,先上Counter.js
的代码:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import * as Actions from './Actions.js';
import { connect } from 'react-redux'
class Counter extends Component {
render() {
const { onIncrement, onDecrement, caption, value } = this.props;
return (
<div>
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
<span>{caption} count: {value}</span>
</div>
);
}
}
Counter.propTypes = {
caption: PropTypes.string.isRequired,
onIncrement: PropTypes.func.isRequired,
onDecrement: PropTypes.func.isRequired,
value: PropTypes.number.isRequired
};
// 用于将`store`的数据加工成当前组件所需要的数据
function mapStateToProps(state, ownProps) {
console.log(state);
console.log(ownProps);
return {
value: state[ownProps.caption]
}
}
// dispatch的操作都被提到这里了,组件瞬间就干净了
function mapDispatchToProps(dispatch, ownProps) {
return {
onIncrement: () => {
dispatch(Actions.increment(ownProps.caption));
},
onDecrement: () => {
dispatch(Actions.decrement(ownProps.caption));
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
这里我们尝试打印mapStateToProps
两个参数,你会发现其实state
就是全局store
,ownProps
就是父级组件传下来的props
你会发现,Counter
组件活生生被抽离成了一个UI组件
,以前我们还在组件中定义包裹dispatch
的方法,定义初始化state
的方法,还需要添加subscribe
订阅store
的变化,但现在,store
的变化感知以及组件state
的初始化都交给了mapStateToProps
来完成,前面说了,只要外层state
变化,都会触发此方法重新渲染。
同理,对于dispatch
方法的定义我们也交给了mapDispatchToProps
来完成,而组件内部只需要通过this.props.onIncrement
就能执行action
派发,是不是相对于之前的写法,简洁了很多呢?
同理,我们修改Summary
组件,代码如下:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'
class Summary extends Component {
render() {
const {sum} = this.props;
return (
<div>Total Count: {sum}</div>
);
}
}
Summary.propTypes = {
sum: PropTypes.number.isRequired
};
function mapStateToProps(state) {
let sum = 0;
for (const key in state) {
if (state.hasOwnProperty(key)) {
sum += state[key];
}
}
return {sum};
}
export default connect(mapStateToProps)(Summary);
保存代码,这个小例子又运行起来,相较于redux
结合context
的写法,react-redux
确实很大程度上精简了代码量。
肆 ❀ 总
那么到这里,我们通过react-redux
再次改写了之前的例子,通过这篇文章,我们知道react-redux
与传统redux
的差异性,从写代码的角度,不得不说react-redux
更加的精简与方便。当然,redux
其实还有不少进阶知识我们还未提及,比如中间件,比如异步处理,再或者对于reducer
的合并等等,这些知识在后面的文章我们会慢慢介绍,那么本文到此结束。
参考
深入浅出React和Redux第三章