Redux Redux Redux Redux Redux Redux Redux Redux Redux But there is an important part of the Redux ecosystem that is not covered, and that is Redux’s asynchronous solution. Redux-thunk this article will explain the official implementation of the asynchronous solution —- redux-thunk, we will start with the basic usage, to the principle of parsing, and then handwritten redux-thunk to replace it, also the source code parsing.

Redux-thunk, as well as Redux and React-Redux, are the work of the official Redux team, with different priorities:

Redux: is a core library with simple functions. It is just a simple state machine, but the underlying idea is not simple. It is the legendary “a hundred lines of code, a thousand lines of document”.

React-redux: the React connection library notifies the React component when Redux status updates.

Redux-thunk: Provides an asynchronous solution to Redux that compensates for Redux functionality.

This article has been uploaded to GitHub, you can take it down to play:Github.com/dennis-jian…

Basic usage

Using our previous counter as an example, to make the counter +1, we would issue an action like this:

function increment() {
  return {
    type: 'INCREMENT'}}; store.dispatch(increment());Copy the code

In the original Redux, Action Creator had to return the Plain Object, and it had to be synchronous. Redux-thunk can be used to issue asynchronous actions, such as timers, network requests, etc.

function increment() {
  return {
    type: 'INCREMENT'}};Async Action Creator
function incrementAsync() {
  return (dispatch) = > {
    setTimeout(() = > {
      dispatch(increment());
    }, 1000); }}// Dispatch with redux-thunk can emit not only plain objects, but also asynchronous functions
store.dispatch(incrementAsync());
Copy the code

Here’s a more practical example, also from the official documentation:

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

// createStore is passed to thunk middleware
const store = createStore(rootReducer, applyMiddleware(thunk));

// A method to initiate a network request
function fetchSecretSauce() {
  return fetch('https://www.baidu.com/s?wd=Secret%20Sauce');
}

// The following two are normal actions
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 that initiates a network request. If it succeeds, makeASandwich, and if it fails, apologize
function makeASandwichWithSecretSauce(forPerson) {
  return function (dispatch) {
    return fetchSecretSauce().then(
      (sauce) = > dispatch(makeASandwich(forPerson, sauce)),
      (error) = > dispatch(apologize('The Sandwich Shop', forPerson, error)),
    );
  };
}

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

Copy the code

Why use itRedux-Thunk?

Before delving further into the source code, let’s consider why redux-thunk is used and why not? Take a closer look at what redux-Thunk does:

Async Action Creator
function incrementAsync() {
  return (dispatch) = > {
    setTimeout(() = > {
      dispatch(increment());
    }, 1000);
  }
}

store.dispatch(incrementAsync());
Copy the code

Redux-thunk allows dispatch to support functions. Before redux-Thunk, the action of dispatch must be a plain object. With Redux-Thunk, the action of Dispatch can support functions. This function passes in the Dispatch itself as an argument. The same effect can be achieved without using redux-thunk. For example, instead of using incrementAsync, I could write:

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

Redux-thunk (redux-thunk) redux-thunk (redux-thunk) redux-thunk (redux-thunk) Stackoverflow has a good answer to this question, and it’s the official recommended explanation. I couldn’t write it any better than he did, so I just translated it:

—- translation from here —-

** Don’t feel like a library should dictate everything! ** If you want to handle a delayed task with JS, use setTimeout, even if you use Redux. Redux does provide another mechanism for handling asynchronous tasks, but you should use it to solve many of your code duplication problems. If you don’t have a lot of repetitive code, using the language native is actually the easiest solution.

Write asynchronous code directly

This is by far the simplest solution, and Redux requires no special configuration:

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

(The function of this code is to display a notification that automatically disappears after 5 seconds, which is the toast effect we often use. The original author always uses this as an example.)

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

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

The only difference is that connected components typically don’t need to use store directly, but instead inject either Dispatch or Action Creator as props, which makes no difference to us.

If you don’t want to write duplicate action names, you can extract both actions as Action Creator rather than dispatch them directly:

// 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)
Copy the code

Or you’ve already injected both action Creators via Connect () :

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

So far, we haven’t used any middleware or other advanced techniques, but we’ve also implemented asynchronous task processing.

Extract the asynchronous Action Creator

Using the above approach works fine in simple scenarios, but you may have noticed several problems:

  1. Every time you want to showtoastYou have to copy this huge piece of code back and forth.
  2. Now,toastThere is noidThis can lead to a competitive situation if you display twice in quick successiontoastWhen the first is over, he willdispatchOut of theHIDE_NOTIFICATION“, which mistakenly causes the second one to be turned off.

To solve these two problems, you may need to extract the toast logic as a method that looks something like this:

// 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 the notification allows the reducer to ignore HIDE_NOTIFICATION that is not the current notification
  // We also record the timer ID so that we can use clearTimeout() to clear the timer later
  const id = nextNotificationId++
  dispatch(showNotification(id, text))

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

Now you use showNotificationWithTimeout components can be directly, don’t have to copy to copy, also don’t have to worry about competition issues:

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

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

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

If you have a single case of store, you can also let showNotificationWithTimeout directly into the store and then dispatch the 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.') 
Copy the code

It doesn’t seem complicated and can work ** but we don’t recommend it! ** The main reason is that your store has to be singleton, which makes Server Render cumbersome to implement. 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 hard to write. It’s hard to mock a Store when testing Action Creator because it references a concrete, real store. You can’t even reset the store state externally.

So technically, you can export a singleton store from a Module, but this is discouraged. Unless you are sure that you will never update Server Render in the future. So let’s go back to the previous scenario:

// 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.')  
Copy the code

This solution solves the problem of duplicate code and competition.

Thunk middleware

For simple projects, the above solution should suffice.

But for large projects, you may still find this inconvenient.

For example, it seems that we have to pass Dispatch as a parameter, which makes it more difficult to separate the container component from the presentation component, because any component that issues an asynchronous Redux action must receive Dispatch as a parameter so that it can pass it down. You can’t just use the connect () to bind the action creator, because showNotificationWithTimeout () is not a real action creator, nor is he returned Redux action.

There is a very embarrassing thing is, you have to remember which action cerator are synchronous, such as showNotification, which is asynchronous auxiliary methods, such as showNotificationWithTimeout. The two are used differently, and you need to be careful not to pass in the wrong parameters or confuse them.

That’s why we need to find a “legal” way to provide dispatch arguments to helper methods and help Redux distinguish which are asynchronous Action Creators so they can be treated differently.

If you are facing similar problems in your projects, welcome the Redux Thunk middleware.

In short, React Thunk tells Redux how to distinguish between specific actions —- it’s actually a function:

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

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

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

// But with Thunk, it can recognize functions
store.dispatch(function (dispatch) {
  // This function can dispatch many actions
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })
  dispatch({ type: 'INCREMENT' })

  setTimeout(() = > {
    // Asynchronous dispatch also works
    dispatch({ type: 'DECREMENT'})},1000)})Copy the code

If you use this middleware and you dispatch a function, React Thunk will pass dispatch itself as an argument. And it eats these function actions, so don’t worry if your Reducer receives strange function parameters. Your Reducer will receive only pure object actions, whether sent directly or from the previous asynchronous functions.

This doesn’t seem like much use, does it? In this case it is! But he let we could like to define a common action creator to define showNotificationWithTimeout:

// 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)}}Copy the code

Note showNotificationWithTimeout here in front of that looks very much like with us, but he does not need to receive the dispatch as the first parameter. Instead, it returns a function that accepts Dispatch as the first argument.

So how to use this function in our component, we can certainly write:

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

So we call the asynchronous Action Creator directly to get the inner function, which needs dispatch, so we give it dispatch.

However, this use is not more awkward, not as good as our previous version! Why are we doing this?

I told you before: With Redux Thunk, if you want to dispatch a function instead of a pure object, the middleware will call the function for you and pass Dispatch as the first argument.

So we can just do it like this:

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

Finally, to a component, dispatch an asynchronous action(which is actually a bunch of regular actions) looks the same as dispatch a regular synchronous action. This is a good thing, because components should not care whether actions are synchronous or asynchronous, which we have abstracted out.

Note that since we’ve taught Redux how to distinguish between these special Action Creators (we call them Thunk Action Creators), we can now use them wherever normal Action Creators are used. 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)
Copy the code

Read State in Thunk

Normally, your Reducer contains logic to calculate the new state, but a Reducer is triggered only when you dispatch an action. What if you have a side effect (such as an API call) in Thunk Action Creator, and in some cases you don’t want to issue the action?

If you don’t have 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.')}Copy the code

But the purpose of extracting Action Creator is to centralize the logic that is repeated across components. Fortunately, Redux Thunk provides a way to read the current Store state. In addition to the Dispatch argument, it passes getState as a second argument so thunk can read the current state of the store.

let nextNotificationId = 0
export function showNotificationWithTimeout(text) {
  return function (dispatch, getState) {
    // Unlike normal action cerator, here we can exit early
    // Redux does not care about the return value, and it does not matter if there is no return value
    if(! getState().areNotificationsEnabled) {return
    }

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

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

But don’t abuse this method! This is great if you need to check the cache to determine whether or not to make AN API request, but it’s not great to base your entire APP logic on it. If you only use getState to conditional dispatch actions, consider putting this logic into the Reducer.

The next step

Now you should be on the working principle of thunk have a basic concept, if you need more examples, we can see here: redux.js.org/introductio… .

You’ll probably find that many examples return promises, which is not necessary, but is handy to use. Redux doesn’t care what value your thunk returns, but it will return it to you via the outer dispatch(). That’s why you can return a Promise in thunk and wait for it to complete:

dispatch(someThunkReturningPromise()).then(...)
Copy the code

Alternatively, you can split a complex Thunk Action Creator into several smaller Thunk Action Creators. This is because thunk provides dispatches that also receive Thunk, so you can always nest dispatch Thunk. And the asynchronous flow can be better controlled by combining Promise’s words.

In more complex applications, you may find your asynchronous control flow difficult to express via Thunk. For example, retrying failed requests, using tokens for reauthorization, or in a step-by-step boot process can be cumbersome and error-prone. If you have these requirements, you can consider some of the more advanced asynchronous process control libraries, such as Redux Saga or Redux Loop. Look at them, evaluate which one fits your needs better, and pick the one you like best.

Finally, don’t use any libraries (including Thunk) if you don’t have real requirements. Remember, our implementation is all about requirements, and your needs may be met with this simple solution:

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

Don’t jump on the bandwagon unless you know why you need it!

—- end of translation —-

StackOverflow’s great god Dan Abramov * * * * the answer to this question is too careful, too in place, after that I watched all dare not to write this reason, this translation to salute the great spirit, stick down the answer again address: stackoverflow.com/questions/3… .

PS: Dan Abramov is the core author of Redux Ecology. These articles are all about Redux, React-Redux, and Redux-Thunk.

The source code parsing

The above translation of the reasons for Redux has already made the scenarios and principles for Redux very clear. Let’s take a look at its source code and write a copy of it to replace it. As usual, let’s start with the main points:

  1. Redux-ThunkIs aReduxMiddleware, so he compliesReduxMiddleware paradigm.
  2. thunkIs the one that candispatchSo we need to rewrite itdispatchLet him accept function arguments.

ReduxMiddleware paradigm

In my previous article on the source code of Redux talked about middleware paradigm and Redux in this source code is how to achieve, have not read or forget friends can go to see. Let me briefly mention that a Redux middleware structure looks something like this:

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
    }
  }
}
Copy the code

A few points to note here:

  1. A middleware receiverstoreAs an argument, a function is returned
  2. The function returned accepts the olddispatchFunctions as arguments (that is, in codenext), returns a new function
  3. The new function returned is newdispatchFunction, the inside of this function can be passed in two layersstoreAnd the olddispatchfunction

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

function thunk(store) {
  return function (next) {
    return function (action) {
      // Return the original result directly
      let result = next(action);
      return result
    }
  }
}
Copy the code

Processing thunk

Thunk is a function that takes dispatch getState, so we should just run thunk, pass it both arguments, and return its return value.

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

      // If action is a function, run it with arguments like Dispatch and getState
      if (typeof action === 'function') {
        return action(dispatch, getState);
      }

      // Otherwise, proceed as normal action
      let result = next(action);
      return result
    }
  }
}
Copy the code

Receives additional arguments withExtraArgument

Redux-thunk also provides an API that lets you use withExtraArgument to inject custom arguments when using applyMiddleware:

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 the extra arguments API and whatever
  };
}
Copy the code

This is also easy to implement, just wrap a layer around the previous thunk function:

CreateThunkMiddleware receives additional parameters
function createThunkMiddleware(extraArgument) {
  return function thunk(store) {
    return function (next) {
      return function (action) {
        const { dispatch, getState } = store;

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

        let result = next(action);
        return result
      }
    }
  }
}
Copy the code

And then our Thunk middleware actually doesn’t add extraArgument:

const thunk = createThunkMiddleware();
Copy the code

The exposed withExtraArgument function is createThunkMiddleware:

thunk.withExtraArgument = createThunkMiddleware;
Copy the code

This is the end of source code parsing. What, that’s it? Yes, that’s it! Redux-thunk is as simple as that, although the idea behind it is quite complex, the code really is only 14 lines! I was shocked, too. Check out the official 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;
Copy the code

conclusion

  1. ifReduxIt’s “a hundred lines of code, a thousand lines of documentation.” ThatRedux-ThunkTen lines of code, a hundred lines of thought.
  2. Redux-ThunkThe main function is to help you give asynchronousactionThe incomingdispatch, so you don’t have to pass it in manually from where it was calleddispatch, which decouples the place of invocation from the place of use.
  3. ReduxandRedux-ThunkLet me deeply understand what is called “programming ideas”, programming ideas can be very complex, but the implementation may not be complex, but it is very useful.
  4. When evaluating whether or not to introduce a library, it is best to consider why we are introducing it and whether there is an easier solution.

This article has been uploaded to GitHub, you can take it down to play:Github.com/dennis-jian…

The resources

Redux-thunk documentation: github.com/reduxjs/red…

Redux-thunk: github.com/reduxjs/red…

Dan Abramov in StackOverflow answer: stackoverflow.com/questions/3…

At the end of this article, thank you for your precious time to read this article. If this article gives you a little help or inspiration, please do not spare your thumbs up and GitHub stars. Your support is the motivation of the author’s continuous creation.

Welcome to follow my public numberThe big front end of the attackThe first time to obtain high quality original ~

“Front-end Advanced Knowledge” series:Juejin. Cn/post / 684490…

“Front-end advanced knowledge” series article source code GitHub address:Github.com/dennis-jian…