It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer. --- Middleware
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-5
// install
npm install
// start
npm start
As our business requirements become more complex, simply handling business logic in dispatch and reducer is no longer universal. What we need is a composable, plug-and-play plugin mechanism. Redux adopts the middleware concept from Koa to implement Redux middleware. Between dispatching an action and executing the reducer, middleware functions modify store.dispatch. Therefore, Redux middleware is designed to enhance dispatch.
Reviewing the usage of middleware, we will take the redux-thunk middleware as an example to add a delayed counting feature to the previous counter, which means incrementing the count two seconds after clicking the button. Redux and react-redux use the official implementation. Below is the modified code:
First, the counter component, in the src/components directory, Counter.js, is as follows:
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Counter extends Component {
constructor(props) {
super(props);
}
render() {
const { value, onIncrement, onDecrement, incrementAsync } = 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>
</p>
)
}
}
Counter.propTypes = {
value: PropTypes.number.isRequired,
onIncrement: PropTypes.func.isRequired,
onDecrement: PropTypes.func.isRequired,
incrementAsync: PropTypes.func.isRequired
};
export default Counter;
In the above code, a button for asynchronous incrementing has been added, which triggers the incrementAsync method in props when clicked.
Next is the code for src/App.js:
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 } = 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}
/>
</div>
);
}
}
const mapStateToProps = (state) => ({
counter: state
});
function onIncrement() {
return { type: 'INCREMENT' }
}
function onDecrement() {
return { type: 'DECREMENT' }
}
function incrementAsync() {
return dispatch => {
setTimeout(() => {
dispatch(onIncrement());
}, 2000)
}
}
const mapDispatchToProps = {
onIncrement,
onDecrement,
incrementAsync
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
Compared to the previous article, the main addition is the incrementAsync method, which is then added to mapDispatch. In incrementSync, it returns a function that performs asynchronous operations to dispatch actions. Ordinary actions are in the form of objects, but asynchronous actions return a function, which requires the use of Redux middleware: redux-thunk.
Below is the code for src/index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import thunk from 'redux-thunk';
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));
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
);
In index.js, the react-thunk module is imported, and then the second parameter is passed to createStore, using Redux's API applyMiddleware to wrap the thunk middleware.
The above is the improved counter. Running the project in the browser, clicking the incrementAsync button achieves the expected effect. As shown in the image below:
Implementing applyMiddleware#
applyMiddleware is the API provided by Redux for using middleware. Reviewing the usage of applyMiddleware:
const store = createStore(counter, applyMiddleware(logger, thunk));
applyMiddleware accepts multiple middleware parameters, and the return value is passed as the second parameter to createStore.
In the src/redux.js file, first, let the original createStore support passing a second parameter, as shown below:
export function createStore(reducer, enhancer) {
if (enhancer) {
return enhancer(createStore)(reducer);
}
}
In createStore, the second parameter enhancer is passed, which is the function that wraps the middleware called applyMiddleware. Then it checks if enhancer exists; if it does, it calls enhancer with createStore and reducer as parameters. Thus, applyMiddleware should be a higher-order function that returns a new function.
Next, expose the applyMiddleware method. Redux middleware supports multiple middleware, but here we first implement support for one middleware usage.
export function applyMiddleware(middleware) {
return createStore => (...args) => {
const store = createStore(...args);
let dispatch = store.dispatch;
const midApi = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
dispatch = middleware(midApi)(store.dispatch)
return {
...store,
dispatch
}
}
}
The structure of applyMiddleware is a multi-layer curried function. The first layer of the function returns a function that is the parameter enhancer in createStore. This function is then passed to createStore, which returns another function that takes the reducer parameter, using ...args for destructuring. Inside the function, it first calls createStore to get the original store and dispatch, then wraps an object midApi to pass to the middleware. midApi includes two methods: getState and dispatch, where getState corresponds to store.getState, and dispatch corresponds to store.dispatch, passing parameters through. Below is a logger middleware code:
const logger = store => next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
As can be seen, the middleware function is a series of wrapped anonymous functions. The first layer takes in store, the second layer takes in next, which is the next middleware, referring to store.dispatch, and the third layer is called when the component is invoked, passing in action. The logger middleware is called layer by layer in applyMiddleware, dynamically assigning values to store and next parameters.
Next, looking at the applyMiddleware code, a closure midApi composed of getState and dispatch is defined. The middleware function middleware is first called with midApi, returning an anonymous function as follows:
next => action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
Then, the anonymous function is called again, passing store.dispatch as the next parameter, returning another anonymous function as follows:
action => {
console.log('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
return result;
};
Through the layered calls of middleware, a new dispatch method is generated, which enhances the original dispatch method of the store. Finally, an object is returned, using destructuring to overwrite the original store.dispatch with the enhanced dispatch, becoming a new store. Ultimately, when dispatch is initiated in the component, the new dispatch method is used.
At this point, Redux has supported the use of middleware. Now let's use it, still using the counter example from the above project, replacing redux and react-redux with our own written files, keeping redux-thunk unchanged. Running the project, clicking the three buttons in sequence achieves the expected effect.
Writing the redux-thunk Middleware#
Now that we have enabled Redux to support middleware, let's try to write our own thunk middleware. Create a new thunk.js in the src directory:
const thunk = ({dispatch, getState}) => next => action => {
return next(action)
}
export default thunk;
The structure of a middleware is shown above, a three-layer arrow function. The first parameter passes in midApi, where dispatch and getState methods are destructured for convenience. The second parameter is the next middleware, which is store.dispatch, and the third parameter is the action to be dispatched. In the middleware function, if nothing is done, it directly returns next(action), which means it directly dispatches the action. Of course, this is not very useful. Below is the code with thunk added:
const thunk = ({dispatch, getState}) => next => action => {
// If it's a function, execute it
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action)
}
export default thunk;
This is the code for redux-thunk, quite simple, right? First, it checks whether the incoming action is a function. If it is, it executes that action and passes dispatch and getState. As shown below, in the parameters of the returned function in incrementAsync, it can accept dispatch and getState:
function incrementAsync() {
return (dispatch, getState) => {
setTimeout(() => {
dispatch(onIncrement());
}, 2000)
}
}
Next, let's verify the implemented thunk middleware by replacing redux-thunk with ./thunk in src. In the browser, click the three buttons in sequence, and the result is as follows, achieving the expected effect.