Redux thunk principle and source code analysis of Redux asynchronous solution

Some time ago, we wrote an article Redux source code analysis article , also This paper analyzes the source code implementation of the library React Redux connected with React . However, there is a very important part not involved in the ecology of Redux, that is, the asynchronous solution of redux. This article will explain the asynchronous solution officially implemented by Redux - Redux thunk. We will start with the basic usage, then the principle analysis, and then hand write a Redux thunk to replace it, that is, source code analysis.

Redux thunk and Redux and react Redux written above are actually the works of the official team of redux. Their focus is different:

Redux: it is a core library with simple functions. It is just a simple state machine, but the idea is not simple. It is the legendary "100 lines of code, 1000 lines of document".

React Redux: it is the connection library with react. When the status of Redux is updated, it notifies the react update component.

Redux thunk: provides an asynchronous solution of Redux to make up for the deficiency of Redux functions.

The handwritten code of this article has been uploaded to GitHub. You can take it down and play: https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/React/redux-thunk/src/myThunk.js

Basic Usage

Or what we did before Take that counter as an example , in order to make the counter + 1, we will issue an action, like this:

function increment() {
  return {
    type: 'INCREMENT'
  }
};

store.dispatch(increment());

In the original Redux, the action creator must return a plain object and must be synchronized. However, our applications often have asynchronous operations such as timers and network requests. You can send asynchronous actions using Redux thunk:

function increment() {
  return {
    type: 'INCREMENT'
  }
};

// Asynchronous action creator
function incrementAsync() {
  return (dispatch) => {
    setTimeout(() => {
      dispatch(increment());
    }, 1000);
  }
}

// After using Redux thunk, dispatch can not only issue plain object, but also issue this asynchronous function
store.dispatch(incrementAsync());

Let's take a more practical example, too Official documents Examples in:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';

// The thunk middleware is passed in when creating the store
const store = createStore(rootReducer, applyMiddleware(thunk));

// Method of initiating network request
function fetchSecretSauce() {
  return fetch('https://www.baidu.com/s?wd=Secret%20Sauce');
}

// The following two are ordinary action s
function makeASandwich(forPerson, secretSauce) {
  return {
    type: 'MAKE_SANDWICH',
    forPerson,
    secretSauce,
  };
}

function apologize(fromPerson, toPerson, error) {
  return {
    type: 'APOLOGIZE',
    fromPerson,
    toPerson,
    error,
  };
}

// This is an asynchronous action. First request the network, make asandwich on success and apologize on failure
function makeASandwichWithSecretSauce(forPerson) {
  return function (dispatch) {
    return fetchSecretSauce().then(
      (sauce) => dispatch(makeASandwich(forPerson, sauce)),
      (error) => dispatch(apologize('The Sandwich Shop', forPerson, error)),
    );
  };
}

// The final dispatch is asynchronous action makeASandwichWithSecretSauce
store.dispatch(makeASandwichWithSecretSauce('Me'));

Why use Redux thunk?

Before going deeper into the source code, let's first think about a question: why should we use Redux thunk without it? Take a closer look at the role of Redux thunk:

// Asynchronous action creator
function incrementAsync() {
  return (dispatch) => {
    setTimeout(() => {
      dispatch(increment());
    }, 1000);
  }
}

store.dispatch(incrementAsync());

It only allows dispath to support one more type, that is, function type. Before using Redux thunk, the action of our dispatch must be a plain object. After using Redux thunk, dispatch can support functions, and this function will pass in the dispatch itself as a parameter. But in fact, we can achieve the same effect without using Redux thunk. For example, in the above code, I can write directly without the outer incrementAsync:

setTimeout(() => {
  store.dispatch(increment());
}, 1000);

In this way, you can also send an increased action after 1 second, and the code is simpler. Why should we use Redux thunk? What is the meaning of its existence? Stack overflow has a good answer to this question and is an officially recommended explanation . I can't write better than him again, so I translated it directly:

----Translation starts here----

Don't think a library should stipulate everything! If you want to handle a delayed task with JS, just use setTimeout directly. Even if you use Redux, it makes no difference. Redux does provide another mechanism for handling asynchronous tasks, but you should use it to solve the problem of a lot of repetitive code. If you don't have too much duplicate code, using the language native scheme is actually the simplest scheme.

Write asynchronous code directly

So far, this is the simplest solution, and Redux does not need special configuration:

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

(Note: the function of this code is to display a notice and disappear automatically after 5 seconds, which is the toast effect we often use. The original author has always taken this as an example.)

Similarly, if you are using in a Redux connected component:

this.props.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  this.props.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

The only difference is that connecting components generally do not need to use store directly, but inject dispatch or action creator as props. These two methods are no different to us.

If you don't want to write duplicate action names, you can extract these two actions into action creator instead of directly dispatch ing an object:

// actions.js
export function showNotification(text) {
  return { type: 'SHOW_NOTIFICATION', text }
}
export function hideNotification() {
  return { type: 'HIDE_NOTIFICATION' }
}

// component.js
import { showNotification, hideNotification } from '../actions'

this.props.dispatch(showNotification('You just logged in.'))
setTimeout(() => {
  this.props.dispatch(hideNotification())
}, 5000)

Or you have injected these two action creator s through connect():

this.props.showNotification('You just logged in.')
setTimeout(() => {
  this.props.hideNotification()
}, 5000)

So far, we haven't used any middleware or other advanced techniques, but we have also realized the processing of asynchronous tasks.

Extract asynchronous Action Creator

Using the above method can work well in simple scenarios, but you may have found several problems:

  1. Every time you want to display toast, you have to copy this large piece of code.
  2. The current toast has no id, which may lead to a competitive situation: if you display toast twice in a row, it will dispatch hide at the end of the first time_ Notification, which will cause the second one to be turned off by mistake.

In order to solve these two problems, you may need to extract the logic of toast as a method, which is roughly as follows:

// actions.js
function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
  // Assigning an ID to a notification allows the reducer to ignore hide that is not the current notification_ NOTIFICATION
  // And we record the ID of the timer so that we can clear the timer later with clearTimeout()
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

Now your components can directly use showNotificationWithTimeout. You don't have to copy around and worry about competition anymore:

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')  

But why does showNotificationWithTimeout() receive dispatch as the first parameter? Because he needs to send the action to the store. General components can get dispatch. In order for external methods to dispatch, we need to give them dispath as a parameter.

If you have a singleton store, you can also let showNotificationWithTimeout directly introduce the store and then dispatch action:

// store.js
export default createStore(reducer)

// actions.js
import store from './store'

// ...

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  const id = nextNotificationId++
  store.dispatch(showNotification(id, text))

  setTimeout(() => {
    store.dispatch(hideNotification(id))
  }, 5000)
}

// component.js
showNotificationWithTimeout('You just logged in.')

// otherComponent.js
showNotificationWithTimeout('You just logged out.') 

This seems not complicated and can achieve results, but we don't recommend it! The main reason is that your store must be singleton, which makes the implementation of Server Render very troublesome. On the Server side, you want each request to have its own store, so that different users can get different preloaded content.

A singleton store also makes unit tests difficult to write. When testing the action creator, it is difficult to mock the store because it refers to a specific real store. You can't even reset the store state from the outside.

So technically, you can export a singleton store from a module, but we don't encourage this. Unless you are sure, you will not upgrade Server Render in the future. So let's go back to the previous scheme:

// actions.js

// ...

let nextNotificationId = 0
export function showNotificationWithTimeout(dispatch, text) {
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

  setTimeout(() => {
    dispatch(hideNotification(id))
  }, 5000)
}

// component.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')

// otherComponent.js
showNotificationWithTimeout(this.props.dispatch, 'You just logged out.')  

This solution can solve the problems of duplicate code and competition.

Thunk Middleware

For simple projects, the above scheme should already meet the requirements.

However, for large projects, you may still find it inconvenient to use this way.

For example, it seems that we must pass dispatch as a parameter, which makes it more difficult for us to separate container components from presentation components, because any component issuing asynchronous Redux action must receive dispatch as a parameter so that it can continue to pass it down. You can't just use connect() to bind the action creator, because showNotificationWithTimeout() is not a real action creator, and it doesn't return a Redux action.

Another embarrassing thing is that you must remember which action certor is synchronous, such as showNotification, and which is asynchronous auxiliary method, such as showNotificationWithTimeout. The usage of the two is different. You need to be careful not to pass the wrong parameters or confuse them.

This is why we need to find a "legal" method to provide dispatch parameters to auxiliary methods and help Redux distinguish asynchronous action creator s for special treatment.

If you face similar problems in your project, welcome to use Redux Thunk middleware.

In short, React Thunk tells Redux how to distinguish this special action - it is actually a function:

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'

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

// This is an ordinary pure object action
store.dispatch({ type: 'INCREMENT' })

// But with Thunk, he can recognize functions
store.dispatch(function (dispatch) {
  // This function can also dispatch many action s
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })

  setTimeout(() => {
    // Asynchronous dispatch can also be used
    dispatch({ type: 'DECREMENT' })
  }, 1000)
})

If you use this middleware and your dispatch is a function, React Thunk will pass the dispatch as a parameter. And he will "eat" these function actions, so don't worry that your reducer will receive strange function parameters. Your reducer will only receive pure object actions, either directly or from the previous asynchronous functions.

This doesn't seem to be much use, does it? In the current example, it is indeed! However, it allows us to define showNotificationWithTimeout as an ordinary action creator:

// actions.js
function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch) {
    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

Note that the showNotificationWithTimeout here looks very similar to the previous one, but it does not need to receive dispatch as the first parameter. Instead, it returns a function to receive dispatch as the first parameter.

How can we use this function in our components? Of course, we can write this:

// component.js
showNotificationWithTimeout('You just logged in.')(this.props.dispatch)

In this way, we directly call the asynchronous action creator to get the inner function. This function needs dispatch as the parameter, so we gave him the dispatch parameter.

However, isn't it more embarrassing to use it like this? It's not as good as our previous version! Why are we doing this?

I told you before: as long as you use Redux Thunk, if you want to dispatch a function instead of a pure object, the middleware will call this function for you and pass in dispatch as the first parameter.

So we can do it directly:

// component.js
this.props.dispatch(showNotificationWithTimeout('You just logged in.'))

Finally, for components, an asynchronous action (actually a bunch of ordinary actions) of dispatch looks no different from an ordinary synchronous action of dispatch. This is a good phenomenon, because components should not care whether those actions are synchronous or asynchronous. We have abstracted it.

Note that because we have taught Redux how to distinguish these special action creators (we call them thunk action creators), now we can use them in any ordinary action creator. For example, we can use them directly in connect():

// actions.js

function showNotification(id, text) {
  return { type: 'SHOW_NOTIFICATION', id, text }
}
function hideNotification(id) {
  return { type: 'HIDE_NOTIFICATION', id }
}

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch) {
    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

// component.js

import { connect } from 'react-redux'

// ...

this.props.showNotificationWithTimeout('You just logged in.')

// ...

export default connect(
  mapStateToProps,
  { showNotificationWithTimeout }
)(MyComponent)

Read State in Thunk

Generally speaking, your reducer will contain the logic to calculate the new state, but the reducer will be triggered only when you dispatch the action. If you have a side effect (such as an API call) in the thunk action creator, what should you do if you don't want to issue this action in some cases?

If there is no Thunk middleware, you need to add this logic to the component:

// component.js
if (this.props.areNotificationsEnabled) {
  showNotificationWithTimeout(this.props.dispatch, 'You just logged in.')
}

However, the purpose of extracting the action creator is to concentrate the logic repeated in various components. Fortunately, Redux Thunk provides a way to read the current store state. That is, in addition to the dispatch parameter, he will also pass in getState as the second parameter, so that thunk can read the current state of the store.

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch, getState) {
    // Unlike ordinary action cerator s, we can exit early here
    // Redux doesn't care about the return value here. It doesn't matter if there is no return value
    if (!getState().areNotificationsEnabled) {
      return
    }

    const id = nextNotificationId++
    dispatch(showNotification(id, text))

    setTimeout(() => {
      dispatch(hideNotification(id))
    }, 5000)
  }
}

But don't abuse this method! If you need to check the cache to determine whether to initiate API requests, this method is very good, but it is not very good to build the logic of your whole APP on this basis. If you only use getState to make conditional judgment on whether to dispatch action, you can consider putting these logic into reducer.

next step

Now you should have a basic concept of thunk's working principle. If you need more examples, you can see here: https://redux.js.org/introduction/examples#async.

You may find that many examples return Promise. This is not necessary, but it is very convenient to use. Redux doesn't care what value your thunk returns, but it will return this value to you through the outer dispatch(). That's why you can return a Promise in thunk and wait for him to finish:

dispatch(someThunkReturningPromise()).then(...)

In addition, you can also split a complex thunk action creator into several smaller thunk action creators. This is because the dispatch provided by thunk can also receive thunk, so you can always nest the dispatch thunk. Moreover, the combination of Promise can better control the asynchronous process.

In some more complex applications, you may find it difficult to express your asynchronous control process through thunk. For example, retrying failed requests, using token for re authorization and authentication, or in the step-by-step guidance process, using this method may be cumbersome and error prone. If you have these requirements, you can consider some more advanced asynchronous process control libraries, such as Redux Saga perhaps Redux Loop . You can look at them, evaluate which is more suitable for your needs, and choose one you like best.

Finally, don't use any libraries (including thunk) if you don't have real requirements. Remember, our implementation depends on the requirements. Maybe your simple scheme can meet your requirements:

store.dispatch({ type: 'SHOW_NOTIFICATION', text: 'You logged in.' })
setTimeout(() => {
  store.dispatch({ type: 'HIDE_NOTIFICATION' })
}, 5000)

Don't follow suit unless you know why you need this!

----That's the end of the translation----

The great God of StackOverflow Dan Abramov The answer to this question is so detailed and in place that I dare not write this reason after reading it. I use this translation to pay tribute to the great God, and then paste the address of this answer: https://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/35415559#35415559.

PS: Dan Abramov is the core author of Redux ecology. Redux, react Redux and Redux thunk mentioned in these articles are all his works.

Source code analysis

In fact, the above translation on the reason has made the applicable scenarios and principles of Redux very clear. Let's take a look at his source code and copy one to replace him. As usual, let's first analyze the main points:

  1. Redux thunk is a Redux middleware, so it follows the paradigm of Redux middleware.
  2. thunk is a function that can dispatch, so we need to rewrite dispatch to accept function parameters.

Redux middleware paradigm

In my previous article on Redux source code, I talked about the paradigm of middleware and how to implement this source code in redux. Friends who have not seen or forgotten can go and have a look again. Let me briefly mention here that a Redux middleware structure is roughly as follows:

function logger(store) {
  return function(next) {
    return function(action) {
      console.group(action.type);
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      console.groupEnd();
      return result
    }
  }
}

Here are some key points:

  1. A middleware receives store as a parameter and returns a function
  2. The returned function takes the old dispatch function as a parameter (that is, next in the code) and returns a new function
  3. The new function returned is the new dispatch function. In this function, you can get the store and the old dispatch function transmitted from the outside two layers

Following this paradigm, let's write the structure of thunk middleware:

function thunk(store) {
  return function (next) {
    return function (action) {
      // First, return the original result directly
      let result = next(action);
      return result
    }
  }
}

Processing thunk

According to what we said earlier, thunk is a function that receives two parameters of dispatch getState, so we should take thunk out and run it, pass them in, and then return its return value directly.

function thunk(store) {
  return function (next) {
    return function (action) {
      // Deconstruct from state
      const { dispatch, getState } = store;

      // If action is a function, take it out and run it. The parameters are dispatch and getState
      if (typeof action === 'function') {
        return action(dispatch, getState);
      }

      // Otherwise, it will be handled as normal action
      let result = next(action);
      return result
    }
  }
}

Receive additional parameter withExtraArgument

Redux thunk also provides an API, that is, when you use applyMiddleware to introduce, you can use withExtraArgument to inject several custom parameters, such as:

const api = "http://www.example.com/sandwiches/";
const whatever = 42;

const store = createStore(
  reducer,
  applyMiddleware(thunk.withExtraArgument({ api, whatever })),
);

function fetchUser(id) {
  return (dispatch, getState, { api, whatever }) => {
    // Now you can use this additional parameter api and whatever
  };
}

This function is also very simple to implement. Just wrap another layer outside the previous thunk function:

// The function createtunkmiddleware in the outer package receives additional parameters
function createThunkMiddleware(extraArgument) {
  return function thunk(store) {
    return function (next) {
      return function (action) {
        const { dispatch, getState } = store;

        if (typeof action === 'function') {
          // When the function is executed here, extraArgument is passed in
          return action(dispatch, getState, extraArgument);  
        }

        let result = next(action);
        return result
      }
    }
  }
}

Then, our thunk middleware is actually equivalent to not transmitting extraArgument:

const thunk = createThunkMiddleware();

The withExtraArgument function exposed to the outside is directly createtunkmiddleware:

thunk.withExtraArgument = createThunkMiddleware;

This is the end of the source code analysis. What, that's it? Yes, that's it! Redux thunk is so simple. Although the idea behind it is complex, the code is really only 14 lines! I was shocked. Let's have a look Official source code Right:

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;

summary

  1. If Redux is "a hundred lines of code, a thousand lines of document", then Redux thunk is "ten lines of code, a hundred lines of thought".
  2. The main function of Redux thunk is to help you pass in dispatch to asynchronous action, so you don't have to manually pass in dispatch from the place of call, so as to realize the decoupling between the place of call and the place of use.
  3. Redux and Redux thunk let me deeply understand what is "programming thought". Programming thought can be very complex, but the implementation may not be complex, but it is very useful.
  4. When we evaluate whether we want to introduce a library, we'd better think about why we want to introduce this library and whether there is a simpler scheme.

The handwritten code of this article has been uploaded to GitHub. You can take it down and play: https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/React/redux-thunk/src/myThunk.js

reference material

Redux thunk document: https://github.com/reduxjs/redux-thunk

Redux thunk source code: https://github.com/reduxjs/redux-thunk/blob/master/src/index.js

Dan Abramov's answer on StackOverflow: https://stackoverflow.com/questions/35411423/how-to-dispatch-a-redux-action-with-a-timeout/35415559#35415559

At the end of the article, thank you for spending your precious time reading this article. If this article gives you a little help or inspiration, please don't be stingy with your praise and GitHub little star. Your support is the driving force of the author's continuous creation.

Author's blog GitHub project address: https://github.com/dennis-jiang/Front-End-Knowledges

I also set up a official account [the big front-end of the attack], no advertising, no hydrology, only high-quality original, welcome to pay attention~

Tags: Front-end React source code analysis redux

Posted by silvio on Thu, 12 May 2022 13:08:56 +0300