高阶组件、展示型和容器型组件搞清楚了之后,就可以来弄一下这个connect
了。
connect
要解决的问题,就是第一篇文章末尾那个问题,到底怎么才能让前端渲染的东西和Redux
结合起来,store的变化又能反映到展示组件上去。
React-Redux
给我们提供了答案,那就是创建一个使用store
的高级组件,把store
的变化映射到React
组件的props
上。来看一下React-Redux
的秘密.
规范地使用React-Redux
在上一篇的末尾,去掉了所有store
相关的代码,为了使用connect
,现在要按照比较规范的方式来使用store
,也就是React-Redux
提供的Provider
组件,在上下文的范围内,让所有组件都可以访问到store
。
使用Provider
为其他组件提供全局store
在index.js
中,使用之前编写好的Reducer
来创建store
,然后通过Provider
组件包裹App
组件:
// index.js
import {Provider} from "react-redux";
export const store = createStore(changeReducer, {value: 1});
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App/>
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
ChangeValue.js
中做好准备
我们的原本意图,是点击两个按钮向store
分派动作,之后让MyResult
获取store
的最新内容并且予以展示,但是遗憾的是如果不进行特殊的操作(比如像第一篇文章一样加入额外的state
),只分派动作并无法让MyResult
刷新。
我们先去掉第二篇文章中那些花里胡哨的高阶组件,将ChangeValue.js修改成如下状态:
import React, {Component} from 'react';
import MyButton from "./MyButton";
import MyResult from "./MyResult";
import {store} from "../index";
import {selfDecrement, selfIncrement} from "../action/actionBuilder";
class ChangeValue extends Component {
constructor(props) {
super(props);
}
handleIncrement = (e) => {
e.preventDefault();
store.dispatch(selfIncrement(1));
}
handleDecrement = (e) => {
e.preventDefault();
store.dispatch(selfDecrement(1));
}
render() {
return (
<div className="App">
<MyButton onClick={this.handleIncrement} text={"+1"}/>
<MyButton onClick={this.handleDecrement} text={"-1"}/>
<MyResult value=???/>
</div>
);
}
}
export default ChangeValue;
两个按钮现在可以向store
派发动作了,现在我们的最大问题就是MyResult
需要有一个value
来展示,在第一篇中已经知道由于MyResult
是无状态组件,store
改变并不会让其刷新,从哪里去获取动态的数据并且刷新呢?
connect()
函数
这里我们正式请出connect()
登场,这个函数是React-redux
库的核心。
这个函数的签名是:
function connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)
四个参数,简介如下:
mapStateToProps
,把store
中存储的state
的属性映射成props
属性,最终传递给被连接的组件mapDispatchToProps
,把派发动作映射为props
属性,最终传递给被连接的组件mergeProps
决定如何处理被连接组件上的各个props
属性,如果不提供,实际上相当于把{ ...ownProps, ...stateProps, ...dispatchProps }
这个props
传递给被连接的组件,即把本来打算传给被连接的组件(现在传递给了连接后的高阶组件)的props
,前两个参数新增的props
,都直接传递给被连接组件。- 配置,可以参考文档
在connect()函数执行完之后,返回的是一个高阶组件,给这个高阶组件再传入被连接的组件作为参数,就得到连接后的组件,写法如下:
const ConnectedComponent = connect(a,b)(OriginalComponent)
最后实际使用的,就是ConnectedComponent
组件。看,这里又是高阶函数柯里化的老套路。
前两个参数非常重要,需要来详细看一下。
mapStateToProps
这个函数的签名是:
mapStateToProps?: (state, ownProps?) => Object
这个函数的用处,是将store
中state
的状态变化,通知到被连接的组件,这样在state
发生变化的时候,组件也会刷新。其内部实际上是订阅了store
的更新。如果这个函数设置为null
,那么被连接的组件将不会在store
更新的时候得到通知。
第一个参数state
,就是store
中存储的state
,即store.getState()
获取的结果。
第二个参数可以不传,如果传递的话,被连接的组件在store
更新或者自己原本的props
更新的时候,都会更新被连接的组件。
这个函数的返回值,必须是一个对象,这个对象会被拆解并传入(合并)到被连接组件的props
中。
mapDispatchToProps
这个函数的签名是:
mapDispatchToProps?: Object | (dispatch, ownProps?) => Object
这个函数的用处和上边很类似,只不过映射的不是state
,而是各种派发动作,在被连接的组件中调用对应的props
就可以来派发传入的动作。
第一个参数是dispatch
,这个实际上就是当前全局对象store
的dispatch
,在返回值中就会使用到这个参数。
第二个参数可以不传,如果传递的话,被连接的组件自己原本的props
更新的时候,都会重新执行这个函数以更新被连接的组件。
这个函数的返回值,必须是一个对象,自然这个对象也会被拆解并传入(合并)到被连接组件的props
中。这个对象要注意,其中每一个键值对的值,是一个派发动作的函数。传入被连接组件之后,被连接组件实际上就可以通过调用传入的函数来派发动作。
其他的东西可以等以后再了解,目前关键就是用好这两个参数。
连接组件
这里我们先连接MyResult
组件,然后来连接MyButton
组件
连接MyResult
组件
要连接MyResult
组件,先需要编写符合mapStateToProps
签名的函数作为参数,之后将这个函数作为参数来调用connect
进行连接,代码如下:
class ChangeValue extends Component {
......
myMapStateToProps = (state) => ({
value: state.value
});
render() {
const ConnectedMyResult = connect(this.myMapStateToProps, null)(MyResult);
return (
<div className="App">
<MyButton onClick={this.handleIncrement} text={"+1"}/>
<MyButton onClick={this.handleDecrement} text={"-1"}/>
<ConnectedMyResult />
</div>
);
}
}
这里需要注意:
myMapStateToProps
返回的对象中value: state.value
这个键值对,键名是将来传递给MyResult
组件的props
属性名,所以必须要和MyResult
组件中所渲染的<h2>{this.props.value}</h2>
一致。
值state.value
,则是根据创建store
时候传入的初始state={value: 1}
得来,注意虽然两个都是value
名称,一个对应被连接的组件,一个对应state
的属性名,千万不要搞错。
如此连接之后,会惊奇的发现,两个按钮派发动作之后,MyResult
也会更新了,此时会发现,整个ChangeValue
组件完全不需要组件内的state
了,也就是说组件的状态都集中到store
中去了。
连接MyButton
组件
一样的考虑,这次需要编写符合mapDispatchToProps
签名的函数来作为connect
的参数。
class ChangeValue extends Component {
......
mapIncrementToProps = (dispatch) => ({
onClick: () => dispatch(selfIncrement(1))
})
mapDecrementToProps = (dispatch) => ({
onClick: () => dispatch(selfDecrement(1))
})
render() {
const ConnectedMyResult = connect(this.myMapStateToProps, null)(MyResult);
const IncreButton = connect(null, this.mapIncrementToProps)(MyButton);
const DecreButton = connect(null, this.mapDecrementToProps)(MyButton);
return (
<div className="App">
<IncreButton text={"+1"}/>
<DecreButton text={"-1"}/>
<ConnectedMyResult />
</div>
);
}
}
这里编写了两个map
函数,分别将onClick
属性对应到自增和自减两个派发动作函数上,最终传递给MyButton
,而MyButton
中本来就是拆解所有属性到button
元素上,所以button
元素的onClick
属性就是对应的派发动作函数,使用了高阶组件之后,无需像原来再给MyButton
传递onClick
属性,这个动作在连接的时候已经做掉了。
回顾一下我们做的所有事情。
- 按照
Redux
的框架要求,编写了action
,创建action
的函数,reducer
,并最终创建了一个store
用于存放状态。 - 按照
React
的框架要求编写了两个按钮和一个展示结果组件,这两个组件本身都是无状态组件 - 使用
React-Redux
将两个按钮连接到派发事件函数,将展示结果组件连接到store
变动。
这样本来没有关系的React
和Redux
,就实现了UI
事件派发动作,store
更新之后刷新UI
这样一个操作,就这个例子来说,与组件内保存state
的区别就是状态集中存放到了另外一个地方。再有其他的组件需要操作store
,只需要再进行连接即可,达到了状态集中管理的目的。
还记得容器型组件和展示型组件吗,connect
之后形成的高阶组件,就是容器型组件,而展示型组件就是被连接的MyButton
之流。
不错不错,趁热打铁终于搞清楚了connect()
,美美睡上一觉,再去看Redux
剩下的部分,比如中间件,拆分reducer
和enhancer
之类,也都是比较高级的内容呢。