Redux 使用 CombineReducer 来组合多个 reducer 函数。
之前我们已经完成了 redux 和 react-redux 的大部分功能,本文将结合之前的 To do list 项目来完善我们编写的 redux 和 react-redux,主要是实现 combineReducer 以及 mapDiapatch 的默认参数和让 mapDispatch 支持 function 参数。
本文完整代码请查看 Github:https://github.com/YanYuanFE/redux-app
// clone repo
git clone https://github.com/YanYuanFE/redux-app.git
cd redux-app
// checkout branch
git checkout part-7
// install
npm install
// start
npm start
combineReducer#
随着应用变得越来越复杂,我们会将 reducer 根据业务进行拆分,拆分后的 reducer 函数负责独立管理 state 的一部分。
Redux 为我们提供了 combineReducer 这个辅助函数,用于将我们拆分后的多个 reducer 根据自身的键值进行组合成一个新的 object,成为新的 reducer,然后对这个 reducer 调用 createStore 方法。
combineReducer 合并后的 reducer 可以调用各个子 reducer,并且把返回的结果合并成一个 state 对象。由 combineReducer 返回的 state 对象,会将传入的每个 reducer 返回的 state 按其传递给 combineReducer 时对应的 key 进行命名。
通常在项目中,我们会为每个单独的 reducer 单独创建 js 文件,在 reducer 中为每个 reducer 进行命名并导出,然后在 reducer 的入口文件中导入,通过 Redux 的 combineReducer 为每个 reducer 进行命名不同的 key 来控制不同的 state 的 key 的命名。
下面回顾一下在之前 Todo list 项目中对 combineReducer 的使用。
reducers/index.js:
import { combineReducers } from 'redux';
import todos from './todos';
import visibilityFilter from './visibilityFilter';
const todoApp = combineReducers({
todos,
visibilityFilter
});
export default todoApp;
combineReducers ({todos, visibilityFilter}) 这里使用了 ES6 的对象简写语法,这与
combineReducers ({todos: todos, visibilityFilter: visibilityFilter}) 是等价的。
此处需要注意,combineReducer 中传入对象的 key 与 redux 中存储的 state 同名。
下面来实现一个简单的 combineReducer。
在 src/redux.js 中:
export function combineReducers(reducers) {
const finalReducerKeys = Object.keys(reducers);
return (state = {}, action) => {
let nextState = {};
finalReducerKeys.forEach((key) => {
const reducer = reducers[key];
const prevStateForKey = state[key];
const nextStateForKey = reducer(prevStateForKey, action);
nextState[key] = nextStateForKey;
});
return nextState;
}
}
可以看到,combineReducer 是一个高阶函数,返回一个 function,首先通过 Object.keys 获取到由 reducers
的 key 组成的数组 finalReducerKeys,然后返回一个 function,这个 function 是一个组合后的 reducer 函数,
接收 state 和 action 参数,state 默认为空对象,在 function 内部,定义 nextState 为空对象,然后对
finalReducerKeys 进行遍历,通过数组的 key 获取到每一个 reducer,然后为 reducer 传入前一个 state 和
action,返回新的 state 并加入到 nextState 的对象中,最后返回新的 state。
我们还可以用更加简洁的代码来实现:
export function combineReducers(reducers) {
const finalReducerKeys = Object.keys(reducers);
return (state = {}, action) => {
return finalReducerKeys.reduce((ret, item) => {
ret[item] = reducers[item](state[item], action);
return ret;
}, {});
}
}
上述代码中,使用 reduce 对一个空对象进行累加操作,对数组每一项进行计算并返回一个新的对象,代码更加简洁。
新的 To do List#
现在我们将之前使用 react-redux 实现的 To do List 项目使用自己实现的 react-redux,修改 containers 文件夹下
的 AddTodo.js、FilterLink.js、VisibleTodoList.js,将引用的 react-redux 修改为自己编写的
react-redux,如下:
import { connect } from '../react-redux'
然后运行项目,开始报错:
emm。
好吧,查看报错信息,发现 containers/FilterLink.js 文件中:
import { connect } from '../react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'
const mapStateToProps = (state, ownProps) => ({
active: ownProps.filter === state.visibilityFilter
})
const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: () => {
dispatch(setVisibilityFilter(ownProps.filter))
}
})
const FilterLink = connect(
mapStateToProps,
mapDispatchToProps
)(Link);
export default FilterLink
在 mapStateToProps 和 mapDispatchToProps 都使用了 ownProps 参数,但是再看下我们自己的 react-redux 中
connect 的参数。
update() {
const { store } = this.context;
const stateProps = mapStateToProps(store.getState());
const dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);
this.setState({
props: {
...this.state.props,
...stateProps,
...dispatchProps,
}
})
}
使用 mapStateToProps 并没有传入 props,修改如下:
const stateProps = mapStateToProps(store.getState(), this.props);
然后,没有报错了,但是页面好像有点问题,下面的筛选按钮没有显示出来。
使用 React 开发者工具查看,是因为 props 没有传递下去。修改上述代码,将 ConectComponent
中的 this.props 也传递下去。
this.setState({
props: {
...this.state.props,
...stateProps,
...dispatchProps,
...this.props,
}
})
再查看界面,显示好了,下面尝试新增一个 to do。输入提交后又报错了。
查看报错信息:
TypeError: dispatch is not a function
原来是 AddTodo 组件的 props 中没有 dispatch 方法,分析如下,使用 connect 将 AddTodo 组件与 Redux 进行连接,
但是 connect 中并没有传递参数,mapDispatch 参数被默认定义为空对象,这里应该默认定义为一个 dispatch 方法,
修改如下:
export const connect = (mapStateToProps = state => state, mapDispatchToProps) => (WrapComponent) => {
return class ConectComponent extends React.Component {
static contextTypes = {
store: PropTypes.object
}
constructor(props, context) {
super(props, context);
this.state = {
props: {}
}
}
componentDidMount() {
const { store } = this.context;
store.subscribe(() => this.update());
this.update();
}
update() {
const { store } = this.context;
const stateProps = mapStateToProps(store.getState(), this.props);
if (!mapDispatchToProps) {
mapDispatchToProps = { dispatch: store.dispatch}
}
this.setState({
props: {
...this.state.props,
...stateProps,
...dispatchProps,
...this.props,
}
})
}
render() {
return <WrapComponent {...this.state.props}/>
}
}
}
这里主要是去掉了 mapDispatch 的默认参数,在 update 函数中对其进行判断是否为空,为空则传递一个对象,对象包含
一个 dispatch 方法。
再次尝试添加 todo,添加成功,但是出来了两条数据,可能是连续触发了两次 dispatch,点击筛选按钮试试,报错了。
查看报错信息:
TypeError: _onClick is not a function
components/Link.js 中的 onClick props 没有传递进来,查看 onClick 方法定义的地方:
在 containers/FilterLink.js 中,在 mapDispatch 中进行了定义:
const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: () => {
dispatch(setVisibilityFilter(ownProps.filter))
}
})
在我们实现的 connect 中,mapDispatch 只支持传递对象参数,下面进行修改,让其支持传递函数。
修改 update 方法:
let dispatchProps;
if (typeof mapDispatchToProps === 'function') {
dispatchProps = mapDispatchToProps(store.dispatch, this.props);
} else {
dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);
}
上述代码的作用主要是对 mapDispatch 进行处理,判断其类型是否为 function,如果为 function 则执行一下,
传入 store.dispatch 和 this.props 参数,返回一个对象,然后赋值到 dispatchProps 上。
然后尝试点击筛选按钮,没有报错,功能正常,但是添加 todo 还是会出现两条数据,经过调试发现是因为又执行了一
次 bindActionCreator,尝试将 mapDispatch 的默认值修改为一个函数,如下:
if (!mapDispatchToProps) {
mapDispatchToProps = (dispatch) => ({dispatch})
}
let dispatchProps;
if (typeof mapDispatchToProps === 'function') {
dispatchProps = mapDispatchToProps(store.dispatch, this.props);
} else {
dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);
}
这样,下面对 mapDispatch 进行判断后,会返回一个对象,包含 dispatch 方法。
重新添加 todo,成功,数据正确。如下图。
细心的你发现了吗?在右边的控制台一直会出现警告,大概意思是我们的组件 props 参数应该是一些数值但是实际上
是 undefined,因为在组件内部定义了 PropTypes。
Link.propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired
}
export default Link
Link.propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired
}
export default Link
问题应该出现在我们的 connect 方法中,在 connect 方法中,最终 render 返回包裹的组件,并传递 this.state.props
到包裹的组件,但是 this.state.props 初始为空,只有在 componentDidMount 才会调用 this.update 方法更新 state,
而组件执行是先 render 再执行 componentDidMount,故第一次 render 时所有的 props 都是 undefined,造成报错。
我们可以将 componentDidMount 修改为 componentWillMount,componentWillMount 在 render 之前执行,可以避免
这个报错。修改如下:
componentWillMount() {
const { store } = this.context;
store.subscribe(() => this.update());
this.update();
}
最终效果如下,没有报错信息,尝试添加 todo 和修改,功能都正常。
参考:
https://github.com/ecmadao/Coding-Guide/tree/master/Notes/React/Redux