Source code repository: github.com/Haixiang612…

Huh? Redux-thunk is a wheel with 14 lines of code. I can write it in one line and you’re gonna teach me how to do it?

Yes, Redux-Thunk is a very small library that can be understood and built in less than five minutes. But today I don’t want to talk about the wheel in terms of how, I want to talk about the wheel in terms of why.

A lot of articles analyzing the redux-thunk source code will say: Pass in dispatch if the action is a function, use dispatch in the action function, and dispatch normally if the action is not a function. However, I feel that this is a results-based approach to why the wheel was built, rather than a requirement-level explanation of what the middleware solves.

In this paper, we hope to derive the reasons for the birth of Redux-Thunk from the perspective of problem solving.

A demand

First, let’s forget about redux-thunk and look at this requirement:

  1. The input box searches for the user Id and calls getUserInfoById to get the user information
  2. The user ID and name are displayed

First, let’s create a store to store userInfo.

// store.js
const initState = {
  userInfo: {
    id: 0.name: 'Technician 0',}}const reducer = (state = initState, action) = > {
  switch (action.type) {
    case 'SET_USER':
      return{... state,userInfo: action.payload} // Update userInfo directly
    default:
      return state
  }
}

const store = createStore(reducer, initState)
Copy the code

Then use the Provider provided by react-Redux to inject data into the entire App:

// App.jsx
function App() {
  return (
    <Provider store={store}>
      <UserInfo/>
    </Provider>)}Copy the code

The last step is to get and display user information in the UserInfo component.

// UserInfo.jsx
const UserInfo = () = > {
  const dispatch = useDispatch()

  const userInfo = useSelector(state= > state.userInfo)

  // Business component status
  const [loading, setLoading] = useState(false)
  const [id, setId] = useState(' ')

  // Get userInfo by Id
  const fetchUserById = (id) = > {
    if (loading) return

    return new Promise(resolve= > {
      setLoading(true)

      setTimeout(() = > {
        const newUserInfo = {
          id: id,
          name: id + 'Technician'
        }

        dispatch({type: 'SET_USER'.payload: newUserInfo})

        setLoading(false)

        resolve(newUserInfo)
      }, 1000)})}return (
    <div>
      <div>
        <input value={id} onChange={e= > setId(e.target.value)}/>
        <button onClick={()= > fetchUserById(id)}>getUserInfo</button>
      </div>

      {
        loading ? <div>Loading in...</div> : (
          <div>
            <p>Id: {userInfo.id}</p>
            <p>Name: {userInfo.name}</p>
          </div>)}</div>)}Copy the code

The above code is very simple: enter id number in input, click “getUserInfo” button to trigger fetchUserById, 1 second later get the latest userInfo to update store value, and finally display technician information.

The decoupling

The above code is so common in many businesses that we don’t need redux-thunk, redux-saga to handle it. You fetch data, send it to action.payload, and dispatch the action to update the value. So many people find it strange when they look at these “frameworks” : these libraries seem to solve some problems, but don’t feel like they’re doing anything big.

What’s wrong with that? If I wanted to pull the fetchUserById out of the component it would be a pain because the entire fetchUserById depends entirely on the Dispatch function. Import store.dispatch (); import store.dispatch ();

// Store must be a singleton
import store from './store'

const fetchUserById = (id) = > {
  if (loading) return

  return new Promise(resolve= > {
    setTimeout(() = > {
      const newUserInfo = {
        id: id,
        name: id + 'Technician'
      }

      store.dispatch({type: 'SET_USER'.payload: newUserInfo})

      resolve(newUserInfo)
    }, 1000)})}Copy the code

But this will result in your store being a singleton! Singletons are bad? Some situations such as SSR, mock store, and test store require multiple stores to exist simultaneously. Therefore, singleton stores are not recommended.

Another way to decouple: We can takedispatchPassed as an argument, rather than using it directly, this completes the decoupling of the function:

// Get userInfo by Id
const fetchUserById = (dispatch, id) = > {
  if (loading) return

  return new Promise(resolve= > {
    setTimeout(() = > {
      const newUserInfo = {
        id: id,
        name: id + 'Technician'
      }

      dispatch({type: 'SET_USER'.payload: newUserInfo})

      resolve(newUserInfo)
    }, 1000)})}// UserInfo.jsx
const UserInfo = (props) = > {
  const dispatch = useDispatch()

  const {userInfo, count} = useSelector(state= > state)

  const [loading, setLoading] = useState(false)
  const [id, setId] = useState(' ')

  const onClick = async () => {
    setLoading(true)
    await fetchUserById(id, dispatch)
    setLoading(false)}return(...). }Copy the code

While the fetchUserById above still looks a bit retarded, the dispatch is passed in only when it is used, completely removing the dependency on dispatch.

Currie,

Each time fetchUserById is executed, a dispatch is passed in, which makes us think: Can in one place the first good fetchUserById initialization, such as initial into fetchUserByIdWithDispatch, let it have the ability to dispatch, And then execute the direct use of fetchUserByIdWithDispatch function?

This is solved by using closures (which can also be used to currize the function), which simply returns one more function:

// Get userInfo by Id
const fetchUserById = (dispatch) = > (id) = > {
  return new Promise(resolve= > {
    setTimeout(() = > {
      const newUserInfo = {
        id: id,
        name: id + 'Technician'
      }

      dispatch({type: 'SET_USER'.payload: newUserInfo})

      resolve(newUserInfo)
    }, 1000)})}Copy the code

FetchUserById = dispatch; fetchUserById = dispatch;

// UserInfo.jsx
const UserInfo = () = > {
  const dispatch = useDispatch()
  
  const {userInfo, count} = useSelector(state= > state)

  const [loading, setLoading] = useState(false)
  const [id, setId] = useState(' ')
  
  const fetchUserByIdWithDispatch = fetchUserById(dispatch)

  const onClick = async () => {
    setLoading(true)
    await fetchUserByIdWithDispatch(id)
    setLoading(false)}return(...). }Copy the code

Defined fetchUserById function in a factory pattern is similar to factories, from its generation fetchUserByIdWithDispatch is our real want “fetchUserById”.

This kind of “functional nesting doll” appears in many of The Wheels of Redux, it is very useful to build the wheel, I hope you can get an impression of this. My own understanding of dealing with an image in this way is that it is like a rocket being prepared for launch. Every time the outer function is executed, it is like adding a little energy to the rocket, and when the last function is executed, the whole rocket is ejected at the highest speed.

Going back to the example, this is also a bad way to declare a function, and it’s still a hassle to initialize it with Dispatch every time you use it. And it can be misleading: a good fetchUserById does not pass an ID but a Dispatch function to initialize it. I’m afraid he’ll follow the wire and hammer you.

Swap the parameters

Our ideal fetchUserById would be used like this:

fetchUserById(id)
Copy the code

Try switching the dispatch and ID to see what happens:

// Get userInfo by Id
const fetchUserById = (id) = > (dispatch) = > {
  return new Promise(resolve= > {
    setTimeout(() = > {
      const newUserInfo = {
        id: id,
        name: id + 'Technician'
      }

      dispatch({type: 'SET_USER'.payload: newUserInfo})

      resolve(newUserInfo)
    }, 1000)})}Copy the code

The onClick component is used like this:

const onClick = async () => {
  setLoading(true)
  const fetchDispatch = fetchUserById(id)
  await fetchDispatch(dispatch)
  setLoading(false)}Copy the code

Although fetchUserById(ID) is ostensibly available, fetchDispatch(Dispatch) is pretty ugly. “FetchUserById” and “dispatch” in reverse, like this:

dispatch(fetchUserById(id))
Copy the code

As a result, any code that uses Dispatch can be wrapped with a function like this:

const fn = (. The parameters of the I) = > (dispatch) = > {
  // Do something with "my parameters"...DoSomthing (My parameters)// dispatch changes the value
  dispatch(...)
}
Copy the code

In case you don’t want to explain the structure again, just generalize it in one word and call it “thunk”.

To do this, we need to change the contents of the Dispatch function to make it an enhanced Dispatch: execute the function’s return function if the input parameter is a function, pass in the dispatch, or dispatch(Action) if it is a normal action.

const originalDispatch = store.dispatch
const getState = store.getState

store.dispatch = (action) = > {
  if (typeof action === 'function') {
    action(originalDispatch, getState)
  } else {
    originalDispatch(action)
  }
}
Copy the code

While it is unsightly to augment Dispatch directly with assignments, a more elegant way is to augment the Dispatch function with middleware functionality provided by Redux.

The middleware

Most people probably can’t write Redux’s middleware yet. In fact, it is very simple, there is a pattern. First, create a template:

const thunkMiddleware = ({dispatch, getState}) = > (next) = > (action) = > {
  next(action) // Pass to the next middleware
}
Copy the code

Redux-thunk is a basic middleware version of the Hello World middleware that does nothing:

const thunkMiddleware = ({dispatch, getState}) = > (next) = > (action) = > {
  if (typeof action === 'function') {
    action(dispatch, getState) // If it is a function, execute that function
  } else {
    next(action) // Pass to the next middleware}}Copy the code

Add applyMiddleware to store. Js:

// store.js
const store = createStore(reducer, initState, applyMiddleware(thunkMiddleware))
Copy the code

Refresh the page and you will find that when executing onClick, there is no loading at all!

const onClick = async () => {
  setLoading(true)
  await dispatch(fetchUserById(id))
  setLoading(false)}Copy the code

This is because fetchUserById returns a Promise, and the middleware doesn’t return it, So setLoading(false) does not wait for the Promise of await Dispatch (fetchUserById(ID)) to return.

To solve this problem, simply add a return to the middleware and simplify the code:

const thunkMiddleware = ({dispatch, getState}) = > (next) = > (action) = > {
  if (typeof action === 'function') {
    return action(dispatch, getState)
  }

  return next(action)
}
Copy the code

One might wonder why action(dispatch, getState) doesn’t pass next instead of dispatch. After all, next will be dispatch at the end of the day, so we have to do the meaning of next and dispatch:

  • Store. dispatch, the dispatch function we use a lot, is actually the dispatch enhanced by all middleware, which can be understood ascompletelyEnhancedDispatch
  • Next, function signatures as well(action) => action, but this is a function in middleware, sort of like enhanced to half dispatch, which is understood aspartiallyEnhancedDispatch

The comparison is as follows:

function type Enhance the degree of Execute the process meaning
dispatch (action) => action Fully enhance Go through the entire middleware flow and call the original at the enddispatch(action) Start the entire distribution process
next (action) => action And a half to enhance Next is used to enter the middleware section, and next is used to return the middleware section Hand it off to the next middleware

infetchUserByIdIn the functiondispatchThe job of the action is to distribute the action through the middleware process, rather than passing it to the next middleware, so the parameter passed to the middleware isdispatchStudent: Functions instead ofnextFunction.

withExtraArgs

In addition to dispatch and getState, developers may also want to pass in additional parameters, such as env.

Create a factory function createThunkMiddleware and pass the extraArgs to the third action parameter:

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

    return next(action)
  }
}

const thunkMiddleware = createThunkMiddleware()

thunkMiddleware.withExtraArgs = createThunkMiddleware

export default thunkMiddleware
Copy the code

Complete parameter passing when used:

// store.js
const store = createStore(
  reducer,
  initState,
  applyMiddleware(thunkMiddleware.withExtraArgs('development')))Copy the code

Finally, fetch “development” from fetchUserById:

// Get userInfo by Id
const fetchUserById = (id) = > (dispatch, getState, env) = > {
  console.log('Current Environment', env)

  return new Promise(resolve= > {
    setTimeout(() = > {
      const newUserInfo = {
        id: id,
        name: id + 'Technician'
      }

      const state = getState()

      dispatch({type: 'SET_USER'.payload: newUserInfo})
      dispatch({type: 'SET_COUNT'.payload: state.count + 1})

      resolve(newUserInfo)
    }, 1000)})}Copy the code

analyse

At this point, we’ve finally implemented the redux-thunk library. Here’s how it works:

  1. We need to complete the acquisition of information and usedispatchThe need to modify store data is supposedly nothing
  2. But it turns out that writing this in a component will depend ondispatchDelta function, so let’s take delta functiondispatchPut it on the parameters
  3. And found that it was passed in every time it was executeddispatchDelta function, which is a hassle, so let’s take delta functiondispatchAs the first argument, write(dispatch) => (id) => {... }This is the structure of the functiondispatchAfter initialization, it can be used everywhere
  4. Finding it cumbersome and misleading to initialize every time, we considered using it(id) => (dispatch) => {... }Function structure, but will appearfetchUserById(id)(dispatch)Such a structure
  5. We want to turn the whole structure around like this:dispatch(fetchUserById(id))So I thought of rewriting itdispatchfunction
  6. It turns out that direct assignment is a stupid behavior, and more advanced is to use middleware to rewrite itdispatchfunction
  7. Finally, we built a middleware called Redux-Thunk

conclusion

Finally, to answer some of the questions I’ve seen in the Redux community.

What problem does Redux-Thunk solve?

Redux-thunk doesn’t solve any real problems, just provides a “Thunk routine” for writing code, which is then automatically “resolved” at dispatch.

Is there any other pattern? Or dispach(acitonPromise) and resolve the Promise in the middleware:

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 }))
          .catch(error= >{ dispatch({ ... action,payload: error, error: true });
            return Promise.reject(error);
          })
      : next(action);
  };
}
Copy the code

Have you got it? How about another NPM package? OK, 70,000 downloads of the Redux-Promise middleware came true in a month. Ah? 70,000 for that simple code? No, I also need to make a pattern by myself, and change the Promise into generator: Dispatch (actionGenerator), which will be another pattern, but this pattern has been registered by Redux-Saga. Uh, with RxJs? But it’s implemented by redux-Observable.

Unfortunately, almost every pattern you can think of has been developed. Currently, redux-Thunk, Redux-Saga, and Redux-Loop are common “pattern parsers” that provide their own set of patterns for developers to dispatch within their frameworks.

It is important to note that redux-Thunk and redux-thunk are not in fact the same level libraries. In addition to providing “translation” of pattern, both redux-Thunk and Redux-Thunk have many features such as error handling, which will not be described here.

Whether dispatch is asynchronous or synchronous

New learners will see await Dispatch (getUserById(ID)) and think that with middleware dispatch is an asynchronous function, but the Redux documentation says that dispatch is synchronous and feels confused.

Parsing out that no matter how many middleware are added, the original dispatch function must be a synchronization function. The reason why we can await it is that the function returned by getUserById is asynchronous. When dispatch(getUserById(ID)) actually executes the return function of getUserById, then Dispatch is indeed asynchronous. However, for ordinary dispatches ({type: ‘SET_USER’, payload:… }) is synchronous.

Do you want to use redux-thunk

If you think it doesn’t matter to me whether you rely on dispatch or not at step 1, it’s easy to use Dispatch directly in the component. You don’t have to manage thunk, saga, and masturbation pages.

Redux-thunk basically just provides a pattern for code writing, which is useful for extracting common code. But don’t overuse thunk, which can easily lead to overdesign.

For example, just this requirement, just take a user information to set up, so little code in the component no questions, not optimization. Even if this code has been used 2 or 3 times, I don’t think it can be optimized so quickly. Unless there are 5 to 7 repetitions and a lot of code, consider extracting as a public function.

Sometimes over-design can lead to serious backbiting and collapse. Redundant code, on the other hand, can lead to incremental optimization in a project where requirements change rapidly. Optimization and repetition are always at the left and right sides of the scale, and a natural balance should be maintained when doing projects, not extremes.