React series — Redux asynchronous stream

Time:2021-9-22

Using Redux to access the server also needs to solve the asynchronous problem.

The Redux one-way data flow is driven by the action object. After each action object is distributed to the store, it is assigned to the reducer function. The reducer returns immediately after completing the data operation. The result returned by the reducer is used to update the status data on the store. The operation of updating the status data will be synchronized to the function monitoring the change of the store status immediately, This triggers the update process of the react view component.

React series -- Redux asynchronous stream

The whole process is executed synchronously all the way. There is no chance of asynchronous operation at all. Where should I insert the asynchronous operation to access the server?

Redux thunk Middleware

Redux thunk middleware is the standard way to solve the asynchronous operation of redux.

npm install redux-thunk --save

Asynchronous actoin object

The driving starting point of Redux one-way data flow is action object, and Redux asynchronous operation can not avoid starting from dispatching an action object. But this action object is special. We call it “asynchronous action object”.

Unlike the ordinary action object (which contains several fields, of which type is essential), the “asynchronous action object” is not an ordinary JavaScript object, but a function.

When an action object of such a function type is dispatched, because there is no type field, there is nothing to do with the reducer in the next step. However, reducer has to intervene automatically according to the steps of Redux data flow. Therefore, if the middleware stands up at this time and believes that this matter must be managed by others, reducer has to cool down.

Therefore, Redux thunk’s job is to check whether the action object is a function, and if not, retreat. If so, execute this function and pass the store’s dispatch function and getstate function as parameters.

A few lines of Redux thunk source code:

function createThunkMiddleware(extraArgument) {
    return ({ dispatch, getState }) => next => action => {
        if(typeof action == 'function') {
            return action(dispatch, getState, extraArgument);
        }
    };
}

It can be clearly seen that when actoin is a function, the next or dispatch method is not called, but the call of the action function is returned.

After understanding the principle of Redux thunk, we simulate an asynchronous request for weather. Action creator can usually be written as follows:

function getWeather(url, params) {
    Return (dispatch, getstate) = > {// the middleware is responsible for calling, and the middleware is also responsible for passing in dispatch and getstate
        fetch(url, params)
            .then(result => {
                dispatch({
                    type: 'GET_WEATHER_SUCCESS',
                    payload: result
                });
            })
            .catch(err => {
                dispatch({
                    type: 'GET_WEATHER_ERROR',
                    error: err
                });
            });
    };
}

The code of asynchronous action function is basically the following routine:

export const sampleAsyncAction = () => {
    return (dispatch, getState) => {
        //In this function, you can call asynchronous functions and decide to dispatch new action objects through the dispatch parameter at an appropriate time
    }
};

This is the working mechanism of asynchronous action. Asynchronous action finally needs to generate the distribution of synchronous actoin in order to reach the response of the view. That’s all Redux thunk has to do. However, because it introduces a function execution, and this function can also access dispatch and getstate, it makes asynchronous operation possible.

In the asynchronous action function, you can initiate an asynchronous request to the server through Ajax. When the result is obtained, you can send the successful or failed result as an actin object through the parameter dispatch. This time, ordinary action objects are distributed, so they will not be intercepted by Redux thunk, but directly arrive at the reducer, and finally drive the change of state on the store.

Redux promise Middleware

We found that asynchronous requests are actually completed by promise, so why not directly solve the asynchronous flow problem by abstracting promise?

npm install redux-promise --save

Analyze how it is done through the source code:

import { isFSA } from 'flux-standard-action';

function isPromise(val) {
    return val && typeof val.then === 'function';
}

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);
    };
}

Redux promise is compatibleFSA standardThat is, the returned results are saved in the payload. The implementation process is very easy to understand, that is, judge whether action or action.payload is promise. If so, execute then and send the returned result to dispatch again.

Using the async and await syntax of ES7, we can simplify the above asynchronous process of obtaining weather:

const fetchData = (url, params) => fetch(url, params);

async function getWeather(url, params) {
    const result = await fetchData(url, params);
    
    if(result.error) {
        return {
            type: 'GET_WEATHER_ERROR',
            error: result.error
        };
    }
    
    return {
        type: 'GET_WEATHER_SUCCESS',
        payload: result
    };
}

redux-composable-fetch

In practice, we also need to add the loading status. Combined with the two open source middleware discussed above, we can implement a middleware that fits the needs of the project. Here, we name it Redux composable fetch.

Ideally, we do not want to request data through complex methods, but want to complete different states in the asynchronous request process in the following forms:

{
    url: '/api/weather.json',
    params: {
        city: encodeURI(city)
    },
    types: ['GET_WEATHER', 'GET_WEATHER_SUCCESS', 'GET_WEATHER_ERROR']
}

As you can see, the action format of asynchronous requests is different from that of FSA. Instead of using the type attribute, it uses the types attribute. Types are actually a collection of three common action types, which represent in request, request success and request failure respectively.

In the request middleware, the format of the action will be checked. If there are URL and types attributes, it indicates that the action is an action used to send asynchronous requests. In addition, not all requests can carry parameters, so params is optional.

When the requesting middleware recognizes that this is an action for sending the request, it will first distribute a new action. The type of this action is the first element in the types array in the original action, that is, in the request. The purpose of distributing this new action is to enable the store to synchronize the status of the current request, such as setting the loading status to true, so that a friendly loading animation can be displayed on the corresponding interface.

Then, the requesting middleware will send an asynchronous request according to the URL, parameters, method and other parameters in the action, and distribute the new actions of successful request and failed request respectively according to the success or failure of the result after the request response.

The simplified implementation of middleware is as follows, which can be modified according to specific scenarios:

const fetchMiddleware = store => next => action => {
    if(!action.url || !Array.isArray(action.types)) {
        return next(action);
    }
    
    const [LOADING, SUCCESS, ERROR] = action.types;
    
    next({
        type: LOADING,
        loading: true,
        ...action
    });
    
    fetch(action.url, { params: action.params })
        .then(result => {
            next({
                type: SUCCESS,
                loading: false,
                payload: result
            });
        })
        .catch(err => {
            next({
                type: ERROR,
                loading: false,
                error: err
            });
        });
};

In this way, we have completed the action of asynchronous request in one step.

redux-observable

In Redux, there are many methods to process asynchronous actions. The most standard method is to use Redux thunk middleware. After being processed by thunk middleware, an action can return a function after being dispatched. This function can be used to do other things: initiate asynchronous requests and dispatch other actions. Using Redux promise is easier to use than Redux thunk, and the complexity is not high. The asynchronous action object created meets the FSA standard.

In the Redux community, Redux sage and Redux observable are also famous.

Redux observable adds corresponding additional effects to each dispatch by creating epics middleware.