[source code analysis] Redux thunk

Posted by LOUDMOUTH on Sun, 16 Jan 2022 20:15:05 +0100

preface

In the previous study of redux, I learned that applymeddleware is in a fog, so this time I will learn the source code of redux thunk, hoping to help me deeply understand the source code implementation of applymeddleware.

Role of Redux thunk

When we use dispatch(action) in redux, the action must be a simple object, but if we want to perform various logical processing during dispatch, such as asynchronous operation, when the action will be a function, we need to use middleware such as redux thunk.

export changeList = (data) =>({
    type: 'CHANGE_LIST',
    data
})

export const getList = (id) => {
    return async(dispatch, getState) => {
        const state = getState();
        const bookName = state.bookName;
        let res = await axios.get('xxxx?id' + id + '&bookName=' + bookName);
        dispatch(changeList(res.data));
    }
}

dispacth(getList)

The idea of Middleware

When middleware is not used:


After using middleware:

The actions distributed to the store will be processed by the middleware layer by layer, and finally reach the store. The order of middleware is closely related to the processing order of action. Only when the previous middleware completes the task, the latter middleware will have the opportunity to continue processing action. Each middleware has its own "fuse" processing. When it thinks that this action does not need to be processed by the later middleware, the latter middleware can no longer process this action.

Middleware basic architecture

When we learned to write logger middleware, we mentioned that the basic architecture of middleware functions is as follows:

const middlewarea = ({dispatch, getState}) => (next) => (action) => {
   next(action);
}

The middleware function accepts two parameters, namely, dispatch and getState. The function returned by the function receives a parameter of type next. If the internal function is called, it means that the middleware has completed its own functions and handed over the control to the next middleware. That is, (action) = > next (action) will be handed over to the next middleware.

(action) = > {} This function can perform a variety of operations:

  • Call dispatch to dispatch a new action object;
  • Call getState to obtain other states on the current store;
  • Call next to tell redux that the current middleware has been called and the next middleware can be called;
  • Access all data of the action object.

Redux thunk source code

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

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

Internally judge the type of action. If it is a non function type, directly call the next middleware (action). If it is a function type, first execute the function action, where the parameters of action are dispatch, getState and arguments respectively.

Here, do we still think it's abstract? Next, let's analyze it in combination with applymeddleware.

Understand the execution process

In the previous section, we said that createStore returns a store object internally. If enhancer is encountered internally, that is, if we use middleware, then the execution result of enhancer will be returned directly. Therefore, the function after enhancer execution must be a store object.

// ...
return enhancer(createStore)(reducer, preloadedState);

Plus

const store = createStore(reducer, preloadedState, applyMiddleware(thunk))

Now we can confirm that the basic architecture of the applyMiddleware function should be as follows:

export const applyMiddleware = (...middlewreas){
    return (createStore) => (reducer, preloadedState) => {
        const store = createStore(reducer, preloadedState);
        // Process the dispatch and replace the new dispatch with the dispatch in the store
        return store;
	}
}

How to handle dispatch, let's continue:

export default function applyMiddleware(...middlewares){
    return (createStore) => (reducer, preloadedState) => {
      const store = createStore(reducer, preloadedState)
      // If the call is made during the middleware construction process, the error prompt is thrown.
      let dispatch = () => {
        throw new Error(
          'Dispatching while constructing your middleware is not allowed. ' +
            'Other middleware would not be applied to this dispatch.'
        )
      }
      const middlewareAPI = {
        getState: store.getState,
        dispatch: (action, ...args) => dispatch(action, ...args)
      }
      // Store the results of multiple middleware calls
      const chain = middlewares.map(middleware => middleware(middlewareAPI))
      // Integrate the chain array with compose and assign it to dispatch
      // compose(f1, f2, f3) = compose(f1(f2(f3())))
      dispatch = compose(...chain)(store.dispatch)
	  // Replace the new dispatch with the original store dispatch
      return {
        ...store,
        dispatch
      }
    }
}

Let's try to figure out what happens when we execute applymeddleware (thunk):

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

    return next(action);
};
// In applyMiddlewrea
// const chain = middlewares.map(middleware => middleware(middlewareAPI))

// Namely
const reduxFn = (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
}
const chain = [reduxFn];
// Continue in applyMiddlewarea
// dispatch = compose(...chain)(store.dispatch)

// The source code of compose has been analyzed earlier. At this time, it is:
dispatch = ((next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
})(store.dispatch)

// That is, after using Redux thunk, the ability of dispatch is enhanced. It can accept both simple objects and functions
dispacth = (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    
    // The next is dispatch
    return next(action)
}

// So if the action is not a function, execute it directly
return dispatch(action)

// If action is a function, run the function
return action(dispatch, getState, extraArgument);
// We wrote this chestnut earlier
export const getList = (id) => {
    return async(dispatch, getState) => {
        // Get other state s in the store
        const state = getState();
        const bookName = state.bookName;
        let res = await axios.get('xxxx?id' + id + '&bookName=' + bookName);
        dispatch(changeList(res.data));
    }
}

// At this point, if you execute dispatch (GetList), then
(async(dispatch, getState) => {
    const state = getState();
    const bookName = state.bookName;
    let res = await axios.get('xxxx?id' + id + '&bookName=' + bookName);
    dispatch(changeList(res.data));
})(dispatch, getState)

summary

The source code of Redux thunk is really short! It's not too difficult to understand the source code, but it's still a little around when combined with applymeddleware. By looking at the source code this time, I found that when I developed it before, I was not sensible years ago. I didn't know that the store value of the function action can be obtained through getState(). Now I have learned.

reference resources

Topics: Javascript