Understanding the principles of Redux helps us use it better. This article implements the functionality of merging multiple middlewares in Redux.
In the previous article, we implemented the middleware mechanism of Redux, supporting the usage of a single middleware. In actual Redux, applyMiddleware supports passing multiple middlewares. This article uses Redux to implement the merging of multiple middlewares.
Please check the complete code in 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-6
// install
npm install
// start
npm start
Middleware Merging#
An example code for using multiple middlewares is as follows:
const store = createStore(
reducer,
applyMiddleware(thunk, promise, logger)
);
Continue developing in the original project, modify the applyMiddleware function in redux.js under src.
export function applyMiddleware(...middlewares) {
}
The structure of a single middleware is as follows:
store => next => action => {
let result = next(action);
return result;
};
When multiple middlewares are passed as parameters, the parameters are spread out, and middlewares become an array for easier manipulation later.
export function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args);
let dispatch = store.dispatch;
const midApi = {
getState: store.getState(),
dispatch: (...args) => dispatch(...args)
}
const middlewareChain = middlewares.map(middleware => middleware(midApi));
dispatch = compose(...middlewareChain)(store.dispatch);
return {
...store,
dispatch
}
}
}
When there are multiple middlewares, the map method is executed on the middleware array middlewares, executing each middleware once and passing in midApi, returning a new array middlewareChain.
middlewareChain holds the functions returned after executing each middleware once [mid1, mid2, mid3], and the structure of each mid is as follows:
next => action => {
let result = next(action);
return result;
};
Then, a compose method is needed to execute each mid method in sequence and return a function, finally passing the store.dispatch parameter.
The function of the compose method is as follows:
compose(fn1, fn2, fn3)
fn1(fn2(fn3)))
Compose takes a series of functions as parameters and then nests and calls them in sequence.
Here is the implementation of compose:
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg;
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((ret, item) => (...args) => ret(item(...args)))
}
In the compose method, a series of function parameters are passed in, and funcs becomes an array.
When funcs.length is 0, meaning there are no parameters, it returns a default function; when one parameter is passed, it directly returns the first parameter; when multiple function parameters are passed, it uses the reduce method of the array to execute a function on the funcs array from left to right. In this function, ret is the return value of the last execution of this function, and if no initial value is specified, it is the first parameter of the array during the first execution, while item is the current array element being processed.
Executing compose(fn1, fn2, fn3), the results of ret and item in the reduce method during each execution are as follows:
During the first execution, ret is fn1, item is fn2, returning fn1(fn2());
During the second execution, ret is fn1(fn2()), item is fn3, returning the result fn1(fn2(fn3())).
At this point, Redux now supports the usage of multiple middlewares.
Writing Middleware for Testing#
To test multiple middlewares, we will write a simple middleware to support array actions, creating redux.array.js in the src directory.
The code is as follows:
const arrayThunk = ({dispatch, getState}) => next => action => {
if (Array.isArray(action)) {
action.forEach(v => dispatch(v))
}
return next(action)
}
export default arrayThunk;
In the above code, the arrayThunk middleware is defined, using the Array.isArray method to check if the action is an array. If it is an array, it iterates through the action and dispatches each one.
Next, we will add a button in the original counter application to use the arrayThunk middleware to increment by 2 each time it is clicked.
Modify components/Counter.js as follows:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Counter extends Component {
constructor(props) {
super(props);
this.incrementAsync = this.incrementAsync.bind(this);
this.incrementIfOdd = this.incrementIfOdd.bind(this);
}
incrementIfOdd() {
if (this.props.value % 2 !== 0) {
this.props.onIncrement();
}
}
incrementAsync() {
setTimeout(this.props.onIncrement, 1000);
}
render() {
const { value, onIncrement, onDecrement, incrementAsync, addTwice } = this.props;
console.log(this.props);
return (
<p>
Clicked: {value} times
{' '}
<button onClick={onIncrement}>
+
</button>
{' '}
<button onClick={onDecrement}>
-
</button>
{' '}
<button onClick={incrementAsync}>
Increment async
</button>
{' '}
<button onClick={addTwice}>
+2
</button>
</p>
)
}
}
Counter.propTypes = {
value: PropTypes.number.isRequired,
onIncrement: PropTypes.func.isRequired,
onDecrement: PropTypes.func.isRequired,
incrementAsync: PropTypes.func.isRequired,
addTwice: PropTypes.func.isRequired,
};
export default Counter;
In Counter.js, a button is added that triggers the addTwice method from props when clicked.
In src, modify the code in App.js as follows:
import React, { Component } from 'react';
import { connect } from './react-redux';
import Counter from './components/Counter';
import logo from './logo.svg';
import './App.css';
class App extends Component {
render() {
const { onIncrement, onDecrement, counter, incrementAsync, addTwice } = this.props;
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
<Counter
value={counter}
onIncrement={onIncrement}
onDecrement={onDecrement}
incrementAsync={incrementAsync}
addTwice={addTwice}
/>
</div>
);
}
}
const mapStateToProps = (state) => ({
counter: state
});
function onIncrement() {
return { type: 'INCREMENT' }
}
function addTwice() {
return [{ type: 'INCREMENT' }, { type: 'INCREMENT' }]
}
function onDecrement() {
return { type: 'DECREMENT' }
}
function incrementAsync() {
return (dispatch, getState) => {
setTimeout(() => {
dispatch(onIncrement());
}, 2000)
}
}
const mapDispatchToProps = {
onIncrement,
onDecrement,
incrementAsync,
addTwice,
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
The addTwice method is added, which initiates an array action, defining two increment actions in the array and adding it to mapDispatch. In the App component, the addTwice method is retrieved from props and passed to the Counter component.
In index.js, the arrayThunk middleware needs to be imported.
import React from 'react';
import ReactDOM from 'react-dom';
import thunk from './thunk';
import arrThunk from './redux-array';
import { Provider } from './react-redux';
import './index.css';
import App from './App';
import {applyMiddleware, createStore} from './redux';
import counter from './reducers';
const store = createStore(counter, applyMiddleware(thunk, arrThunk));
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
);
In index.js, import arrayThunk and pass it to applyMiddleware.
Start the project with npm start, open the browser to operate, and the result is as follows, achieving the expected effect.