Redux uses CombineReducer to combine multiple reducer functions.
Previously, we have completed most of the functionalities of redux and react-redux. This article will improve our implementation of redux and react-redux in conjunction with the previous To do list project, mainly to implement combineReducer and the default parameters of mapDispatch, and to allow mapDispatch to support function parameters.
Please check the complete code of this article on 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#
As applications become more complex, we will split reducers according to business logic. The split reducer functions are responsible for independently managing a part of the state.
Redux provides us with the combineReducer helper function, which is used to combine multiple split reducers into a new object based on their keys, becoming a new reducer, and then calling the createStore method on this reducer.
The reducer returned by combineReducer can call each sub-reducer and merge the returned results into a state object. The state object returned by combineReducer will name the state returned by each reducer according to the key corresponding to the reducer when passed to combineReducer.
Typically in a project, we will create separate js files for each individual reducer, naming and exporting each reducer in the reducer files, and then importing them in the entry file of the reducers, using Redux's combineReducer to name different keys for each reducer to control the naming of different state keys.
Let's review the use of combineReducer in the previous Todo list project.
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}) here uses the ES6 object shorthand syntax, which is equivalent to
combineReducers({todos: todos, visibilityFilter: visibilityFilter}).
It is important to note that the keys of the object passed into combineReducer must match the names of the states stored in redux.
Now let's implement a simple combineReducer.
In 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;
}
}
As we can see, combineReducer is a higher-order function that returns a function. First, it retrieves an array of keys composed of the reducers using Object.keys, which is stored in finalReducerKeys. Then it returns a function, which is a combined reducer function that takes state and action as parameters, with state defaulting to an empty object. Inside the function, nextState is defined as an empty object, and then it iterates over finalReducerKeys, retrieving each reducer using the array's key, passing the previous state and action to the reducer, returning the new state and adding it to the nextState object, and finally returning the new state.
We can also implement it with more concise code:
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;
}, {});
}
}
In the above code, we use reduce to accumulate an empty object, calculating each item in the array and returning a new object, making the code more concise.
New To do List#
Now we will modify the To do List project that was previously implemented using react-redux to use our own implementation of react-redux, modifying the AddTodo.js, FilterLink.js, and VisibleTodoList.js files in the containers folder, changing the imported react-redux to our own written react-redux, as follows:
import { connect } from '../react-redux'
Then run the project, and an error occurs:
emm.
Okay, checking the error message, I found in the containers/FilterLink.js file:
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
Both mapStateToProps and mapDispatchToProps use the ownProps parameter, but looking at our own react-redux, the parameters of 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,
}
})
}
Using mapStateToProps does not pass in props, modify as follows:
const stateProps = mapStateToProps(store.getState(), this.props);
Then, there were no errors, but the page seemed to have some issues, the filter buttons below did not show up.
Using React Developer Tools to check, it was because props were not passed down. Modify the above code to also pass this.props in the ConectComponent.
this.setState({
props: {
...this.state.props,
...stateProps,
...dispatchProps,
...this.props,
}
})
After checking the interface again, it displayed correctly. Next, I tried to add a to do. After submitting, it reported an error again.
Checking the error message:
TypeError: dispatch is not a function
It turned out that the props of the AddTodo component did not have the dispatch method. Analyzing this, the AddTodo component was connected to Redux using connect, but no parameters were passed in connect, and the mapDispatch parameter was defaulted to an empty object. Here, it should be defined as a dispatch method by default, modify as follows:
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}/>
}
}
}
Here, the main change is to remove the default parameter for mapDispatch, and in the update function, check if it is empty. If it is empty, pass an object that contains a dispatch method.
Trying to add a todo again, it succeeded, but two entries appeared, possibly due to dispatch being triggered twice. Let's try clicking the filter button, and see if it reports an error.
Checking the error message:
TypeError: _onClick is not a function
The onClick prop in components/Link.js was not passed in. Checking where the onClick method is defined:
In containers/FilterLink.js, it was defined in mapDispatch:
const mapDispatchToProps = (dispatch, ownProps) => ({
onClick: () => {
dispatch(setVisibilityFilter(ownProps.filter))
}
})
In our implementation of connect, mapDispatch only supports passing object parameters. Let's modify it to support passing functions.
Modify the update method:
let dispatchProps;
if (typeof mapDispatchToProps === 'function') {
dispatchProps = mapDispatchToProps(store.dispatch, this.props);
} else {
dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);
}
The above code mainly processes mapDispatch, checking its type to see if it is a function. If it is a function, it executes it, passing store.dispatch and this.props as parameters, returning an object, and then assigning it to dispatchProps.
Then, trying to click the filter button, there were no errors, and the functionality was normal. However, adding a todo still resulted in two entries. After debugging, it was found that it was due to bindActionCreator being executed again. Let's try changing the default value of mapDispatch to a function as follows:
if (!mapDispatchToProps) {
mapDispatchToProps = (dispatch) => ({dispatch})
}
let dispatchProps;
if (typeof mapDispatchToProps === 'function') {
dispatchProps = mapDispatchToProps(store.dispatch, this.props);
} else {
dispatchProps = bindActionCreators(mapDispatchToProps, store.dispatch);
}
This way, after checking mapDispatch, it will return an object containing the dispatch method.
Adding a todo again, it succeeded, and the data was correct. As shown in the figure.
Have you noticed? There has been a warning in the console on the right, indicating that the props parameter of our component should be some values but is actually undefined, because PropTypes is defined inside the component.
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
The problem should lie in our connect method. In the connect method, the final render returns the wrapped component and passes this.state.props to the wrapped component, but this.state.props is initially empty. The update method is only called in componentDidMount to update the state, and the component executes render before componentDidMount, so all props are undefined during the first render, causing the error.
We can change componentDidMount to componentWillMount, which executes before render, to avoid this error. Modify as follows:
componentWillMount() {
const { store } = this.context;
store.subscribe(() => this.update());
this.update();
}
The final effect is as follows, with no error messages. Trying to add and modify todos, all functionalities are normal.
Reference:
https://github.com/ecmadao/Coding-Guide/tree/master/Notes/React/Redux