Redux introductory tutorial (two): middleware and asynchronous operation
In the last article , I introduced the basic approach of Redux: the user sends out an Action, the Reducer function calculates a new State, and the View re-renders.
However, a key issue has not been resolved: what about asynchronous operations? After the action is sent, the Reducer calculates the State immediately, which is called synchronization; after the action is sent, the Reducer is executed after a period of time, which is asynchronous.
How can the Reducer be executed automatically after the asynchronous operation ends? This requires a new tool: middleware.
One, the concept of middleware
In order to understand the middleware, let us think about the problem from the perspective of the framework author: If you want to add features, where will you add it?
(1) Reducer: Pure function, which only undertakes the function of calculating State. It is not suitable to undertake other functions, nor can it be undertaken, because in theory, pure functions cannot perform read and write operations.
(2) View: One-to-one correspondence with State, which can be regarded as the visual layer of State, and it is not suitable for other functions.
(3) Action: The object that stores the data, that is, the carrier of the message, can only be operated by others, and cannot perform any operation by itself.
After much deliberation, only this step of sending Action, that is, the
store.dispatch()
method, can add functions.
For example, if you want to add a log function and print out the Action and State, you can modify
store.dispatch
it as follows.
let next = store.dispatch; store.dispatch = function dispatchAndLog(action) { console.log('dispatching', action); next(action); console.log('next state', store.getState()); }
In the above code, the
store.dispatch
redefinition is carried out, and the printing function is added before and after sending the Action.
This is the embryonic form of middleware.
The middleware is a function. The
store.dispatch
method is transformed, and other functions are added between the two steps of issuing Action and executing Reducer.
Second, the usage of middleware
This tutorial does not involve how to write middleware, because commonly used middleware is readily available, as long as you refer to the modules written by others. For example, in the log middleware of the previous section, there is a ready-made redux-logger module. Here only introduces how to use middleware.
import { applyMiddleware, createStore } from 'redux'; import createLogger from 'redux-logger'; const logger = createLogger(); const store = createStore( reducer, applyMiddleware(logger) );
In the above code,
redux-logger
a generator is provided
createLogger
to generate log middleware
logger
.
Then, put it in the
applyMiddleware
method, pass in the
createStore
method, and complete the
store.dispatch()
function enhancement.
There are two points to note here:
(1) The
createStore
method can accept the initial state of the entire application as a parameter, in that case, it
applyMiddleware
is the third parameter.
const store = createStore( reducer, initial_state, applyMiddleware(logger) );
(2) The order of middleware is exquisite.
const store = createStore( reducer, applyMiddleware(thunk, promise, logger) );
In the above code,
applyMiddleware
the three parameters of the method are three middleware.
Some middleware have order requirements, please check the document before use.
For example,
logger
it must be put at the end, otherwise the output result will be incorrect.
Three, applyMiddlewares()
Seeing this, you may ask,
applyMiddlewares
what exactly is this method for?
It is a native method of Redux, whose function is to form an array of all middleware and execute them in sequence. Below is its source code.
export default function applyMiddleware(...middlewares) { return (createStore) => (reducer, preloadedState, enhancer) => { var store = createStore(reducer, preloadedState, enhancer); var dispatch = store.dispatch; var chain = []; var middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) }; chain = middlewares.map(middleware => middleware(middlewareAPI)); dispatch = compose(...chain)(store.dispatch); return {...store, dispatch} } }
In the above code, all the middleware are put into an array
chain
, then nested execution, and finally executed
store.dispatch
.
It can be seen inside the middleware (
middlewareAPI
) can get
getState
and
dispatch
these two methods.
Fourth, the basic idea of asynchronous operation
After understanding the middleware, you can handle asynchronous operations.
Synchronous operations only need to send out one Action. The difference between asynchronous operations is that it sends out three kinds of Actions.
- Action when the operation is initiated
- Action when the operation is successful
- Action when the operation fails
Take fetching data from the server as an example, the three actions can be written in two different ways.
// 写法一:名称相同,参数不同 { type: 'FETCH_POSTS' } { type: 'FETCH_POSTS', status: 'error', error: 'Oops' } { type: 'FETCH_POSTS', status: 'success', response: { ... } } // 写法二:名称不同 { type: 'FETCH_POSTS_REQUEST' } { type: 'FETCH_POSTS_FAILURE', error: 'Oops' } { type: 'FETCH_POSTS_SUCCESS', response: { ... } }
In addition to the different types of Actions, the State of asynchronous operations must also be transformed to reflect different operating states. The following is an example of State.
let state = { // ... isFetching: true, didInvalidate: true, lastUpdated: 'xxxxxxx' };
In the above code, the State attribute
isFetching
indicates whether data is being fetched.
didInvalidate
Indicates whether the data is out of date,
lastUpdated
and the time of the last update.
Now, the whole idea of asynchronous operation is very clear.
- When the operation starts, an Action is sent, which triggers the State to be updated to the “operating” state, and the View is re-rendered
- After the operation is over, another Action is sent, which triggers the State to be updated to the “operation end” state, and the View is rendered again
Five, redux-thunk middleware
Asynchronous operations must send at least two Actions: the user triggers the first Action, which is the same as a synchronous operation, there is no problem; how can the system automatically send the second Action when the operation ends?
The mystery is in Action Creator.
class AsyncApp extends Component { componentDidMount() { const { dispatch, selectedPost } = this.props dispatch(fetchPosts(selectedPost)) } // ...
The above code is an example of an asynchronous component.
After loading successfully (
componentDidMount
method), it sends (
dispatch
method) an Action to request data from the server
fetchPosts(selectedSubreddit)
.
Here
fetchPosts
is Action Creator.
Below is
fetchPosts
the code, the key is inside.
const fetchPosts = postTitle => (dispatch, getState) => { dispatch(requestPosts(postTitle)); return fetch(`/some/API/${postTitle}.json`) .then(response => response.json()) .then(json => dispatch(receivePosts(postTitle, json))); }; }; // 使用方法一 store.dispatch(fetchPosts('reactjs')); // 使用方法二 store.dispatch(fetchPosts('reactjs')).then(() => console.log(store.getState()) );
In the above code, it
fetchPosts
is an Action Creator, which returns a function.
After this function is executed, an Action (
requestPosts(postTitle)
) is
issued first
, and then an asynchronous operation is performed.
After getting the result, first convert the result into JSON format, and then send out an Action (
receivePosts(postTitle, json)
).
In the above code, there are several places that need attention.
(1)
fetchPosts
A function is returned, and the normal Action Creator returns an object by default.(2) The parameter of the returned function is the two Redux methods,
dispatch
and the parameter of thegetState
ordinary Action Creator is the content of the Action.(3) In the returned function, first issue an Action (
requestPosts(postTitle)
) to indicate the start of the operation.(4) After the asynchronous operation ends, another Action (
receivePosts(postTitle, json)
) is issued to indicate the end of the operation.
This processing solves the problem of automatically sending the second Action.
However, it brings a new problem, Action is
store.dispatch
sent
by the
method.
In the
store.dispatch
normal case of a method, the parameter can only be an object, not a function.
At this time, it is necessary to use middleware
redux-thunk
.
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import reducer from './reducers'; // Note: this API requires redux@>=3.1.0 const store = createStore( reducer, applyMiddleware(thunk) );
The above code uses
redux-thunk
middleware and is modified
store.dispatch
so that the latter can accept functions as parameters.
Therefore, the first solution for asynchronous operation is to write an Action Creator that returns a function, and then use
redux-thunk
middleware to transform it
store.dispatch
.
Six, redux-promise middleware
Since Action Creator can return functions, of course it can also return other values. Another solution for asynchronous operations is to let Action Creator return a Promise object.
This requires the use of
redux-promise
middleware.
import { createStore, applyMiddleware } from 'redux'; import promiseMiddleware from 'redux-promise'; import reducer from './reducers'; const store = createStore( reducer, applyMiddleware(promiseMiddleware) );
This middleware allows
store.dispatch
methods to accept Promise objects as parameters.
At this time, Action Creator has two ways of writing.
Writing one, the return value is a Promise object.
const fetchPosts = (dispatch, postTitle) => new Promise(function (resolve, reject) { dispatch(requestPosts(postTitle)); return fetch(`/some/API/${postTitle}.json`) .then(response => { type: 'FETCH_POSTS', payload: response.json() }); });
Writing method two, the
payload
property of the
Action object
is a Promise object.
This requires the
redux-actions
introduction of
createAction
methods
from the
module
, and the writing method has to be as follows.
import { createAction } from 'redux-actions'; class AsyncApp extends Component { componentDidMount() { const { dispatch, selectedPost } = this.props // 发出同步 Action dispatch(requestPosts(selectedPost)); // 发出异步 Action dispatch(createAction( 'FETCH_POSTS', fetch(`/some/API/${postTitle}.json`) .then(response => response.json()) )); }
In the above code, the second
dispatch
method sends out an asynchronous Action, and this Action will only be sent out until the end of the operation.
Note that
createAction
the second parameter must be a Promise object.
If you look at
redux-promise
the
source code
, you will understand how it operates internally.
export default function promiseMiddleware({ dispatch }) { return next => action => { if (!isFSA(action)) { return isPromise(action) ? action.then(dispatch) : next(action); } return isPromise(action.payload) ? action.payload.then( result => dispatch({ ...action, payload: result }), error => { dispatch({ ...action, payload: error, error: true }); return Promise.reject(error); } ) : next(action); }; }
As can be seen from the above code, if the Action itself is a Promise, its value after resolve should be an Action object, which will be
dispatch
sent
by the
method (
action.then(dispatch)
), but there will be no action after reject; if the
payload
property of the
Action object
is a Promise object , Then no matter resolve and reject, the
dispatch
method will issue Action.
That’s it for middleware and asynchronous operations.
The next article
will be the last part, which will introduce how to use
react-redux
this library.
(over)