This problem comes from a bug found accidentally in the project. The scenario is that the component will re-render after switching the project. After each re-render, the component will request the data of the corresponding current project after mounting, save it in store and display it on the page.

But the duration of online requests is unpredictable, which raises a question:

Switch to project B, the request has been sent but hasn’t come back, then switch to project A. So now there are two requests, the previous request B and the new request A.

The speed of request A is relatively fast, and it succeeds immediately. The data is stored in store and displayed.

But after a while, request B comes back and puts the data in store. But this is under project A, and the data is originally for A, and then replaced with the data for REQUEST B. This is where the problem arises.

This is the request race problem, as it is now:

To simulate the effect, an interface is added to webpackDevServer to control the response time based on project parameters

before(app, server){
  app.get('/api/request/:project'.function (req, res) {
    const { project } = req.params
    if (project === 'B') {
      setTimeout(() => {
        res.send({result: 'Result of B'})
        res.status(200)
      }, 4000)
      return
    }
    setTimeout(() => {
      res.send({result: 'Result of A'})
      res.status(200)
    }, 1000)
  })
}
Copy the code

The solution

For this problem alone, there is a simple way to store the requested data in the component. Since the previous component is destroyed after switching the project, the new component is displayed, and the problem will not occur.

One thing to note, however, is that this approach requires canceling the request when the component is destroyed, or setting state after the request is successful to determine whether the component is destroyed, because we cannot set state in a destroyed component.

A more suitable

However, in the case of existing projects, often the data placed in the store needs to be relied on elsewhere and cannot be stored in components.

In this case, consider how to control as little data as possible.

The problem now is that two requests, one fast and one slow, overwrite the data of the fast request.

So what’s the point of control? This is when the data is stored in the store after the request is successful.

How do you control it? Whether the successful request data is updated to the store.

So how do you tell if you should update to store? It would be nice to be clear that switching projects causes one request to be sent first and two requests to be sent later, which must correspond to the current switched project. You can determine whether data should be stored by identifying whether the current request is a later one. To determine this, you need to store a time in the store to record when the request was made: reqStartLastTime

The specific practices

When a request is made, a timestamp is obtained as the start time of the current request, and when the request completes, this time point is recorded in the store. So when the next request comes in, this time will be the same as the last time the request was sent.

When the request is successful, the last time point is obtained from the Store. Compared with the time when the request is sent, the data is stored in the Store only when the time when the request is sent is longer than the time when the request is sent last time.

In combination with the scenario, switch to project B, send request to record the time point of B, and then switch to A, send request to record the time point of A. At this point, two points in time are recorded in A > B > store.

A requests to return first, judge, A time point > store record time point, pass, store data, update A time point to store, after A while B comes back, verify B time point < A time point, do not pass, do not save data.

export const fetchData = project => {
  return (dispatch, getState) => {
    const reqStartThisTime = Date.now()
    dispatch({ type: FETCH_BEGIN })
    fetch(`/api/request/${project}').then(res => res.json()).then(res => {// Get the last request from store reqStartLastTime const {view: {reqStartLastTime}} = getState() {reqStartLastTime}} = getState()if (reqStartThisTime > reqStartLastTime) {
          dispatch({
            type: FETCH_SUCCESS, payload: res.result, }) } }).finally(() => { const { view: {reqStartLastTime}} = getState() {reqStartLastTime}} = getState();type: RECORD_REQUEST_START_POINT, payload: reqStartThisTime > reqStartLastTime? Thistime: reqStartLastTime})})}}Copy the code

The effect is as follows, focusing on the order of request 200:

At this point, the problem has been solved, but it’s still not perfect, because it’s not the only place that needs to be dealt with.

further

The above provides a solution for a particular scenario. Control whether or not the results of a request should be stored in a store by setting, retrieving, and comparing the date of the request in an asynchronous action, but such scenarios can be numerous, resulting in repetitive logic. Encapsulate the repetitive logic as much as possible and focus on the business, which leads to Redux’s middleware concept.

Redux’s Middleware’s goal is to modify The Dispatch, but it doesn’t have to. You can put some generic logic into the middleware and call the Dispatch directly.

Review the process above in asynchronous actions:

  • When a request is sent, the current time is recorded as the request time
  • When the request completes, compare the time of this issue with the time of the last issue, and record the large ones to store
  • When the request is successful, compare the sending time with the last sending time, once the former is greater than the latter, put the data into store.

As you can see, only the logic of the first two steps can be separated from the business. Since the action is completely within the callback of the successful request, the dispatch is the real action and cannot be executed.

It is now possible to decide whether to store the result of a request by abstracting the logic that stores the request time into the middleware, while in the business code of an asynchronous action, it simply retrieves this request time from the store and compares it with the last request time.

Note that only one time is recorded in the store, which is the last time the request was issued, but it is also abstracted from the present time, so we need to add another field in the store, which is the current time the request was issued

Creating a new middleware can actually integrate the logic of Redux-Thunk and add the logic to be abstracted.

function reqTimeControl({ dispatch, getState }) {
  return next => {
    return action => {
      if (typeof action === 'function') {
        const result = action(dispatch, getState);
        if (result && 'then' inConst thisTime = date.now () next({const thisTime = date.now () next({const thisTime = date.now () next({const thisTime = date.now () next()type: '__RECORD_REQUEST_THIS_TIME__', payload: ThisTime}) const {reqStartLastTime, reqStartThisTime} = getState() result.finally(() => {// request completed Update the time in store to largeif (reqStartThisTime > reqStartLastTime) {
              next({
                type: '__RECORD_REQUEST_START_POINT__',
                payload: reqStartThisTime
              })
            }
          })
        }
        return result
      }
      return next(action)
    }
  }
}

export default reqTimeControl
Copy the code

On passing in applyMiddleware, replace redux-thunk

import { global, view, reqStartLastTime, reqStartThisTime } from './reducer'
import reqTimeControl from './middleware'

const store = createStore(
  combineReducers({ global, view, reqStartLastTime, reqStartThisTime }),
  applyMiddleware(reqTimeControl),
)
export default store
Copy the code

To let the middleware know the status of the request, fetch that returns a promise needs to be returned in the asynchronous action. Now you just need to get two times to compare and decide whether to update the data.

export const fetchData = project => {
  return (dispatch, getState) => {
    dispatch({
      type: FETCH_BEGIN,
    })
    return fetch(`/api/request/${project}`)
      .then(res => res.json())
      .then(res => {
        const { reqStartLastTime, reqStartThisTime } = getState()
        if (reqStartThisTime > reqStartLastTime) {
          dispatch({
            type: FETCH_SUCCESS,
            payload: res.result,
          })
        }
      })
  }
}
Copy the code

As you can see, abstracting logic to middleware still works, but you only care about business processing.

Redux change

Also don’t finish calculate

The storage store business logic is now in the callback for a successful request, and the middleware has no control over the actions dispatched here. So the second method still doesn’t completely get rid of the repetition logic, it still needs to match the promise that returns fetch in the business, and it needs to get two times from the store for comparison.

Middleware is here again

Is there a way to put all the time-related processing logic in one place so that the business code still contains only actions, unaware of this layer of processing? Sure, but let the middleware take over the request. Because this layer of processing runs through the entire network request cycle.

How do you let the middleware take over the request, and how do you let the middleware do all of this control? Think about it. In middleware, you can get the result of an asynchronous action (a function called Dispatch), but in this asynchronous action, the request is not sent. The request is handled by the middleware, which listens for a signal to send a request in the return result.

So, modify the boilerplate code for asynchronous actions that require requests:

export const fetchData = project => {
  return dispatch => {
    return dispatch(() => {
      returnFETCH_BEGIN, FETCH_SUCCESS, FETCH_FAILURE, FETCH: {types: [ FETCH_BEGIN, FETCH_SUCCESS, FETCH_FAILURE ], url: `/api/request/${project}'},}})}}Copy the code

The request should be sent based on the result returned.

function reqTimeControl({ dispatch, getState }) {
  return next => {
    return action => {
      if (typeof action === 'function') {
        let result = action(dispatch, getState);
        if (result) {
          if ('FETCH' inResult) {const {FETCH} = result // dispatch action next({type: FETCH. Types [0]}) // Result is set to promise to ensure that the call function in the component is a promise, so that there is more business freedom in the component based on the state of the promise, Promise.all result = fetch(fetch.url).then(res => res.json()).then(res => {next({type: FETCH.types[1],
                payload: res
              })
              return res
            }).catch(error => {
              next({
                type: FETCH.types[2],
                payload: error,
              })
              return error
            })
          }
        }
        return result
      }
      return next(action)
    }
  }
}

export default reqTimeControl
Copy the code

Note that the fetch is returned as a result so that the function we call in the component to fetch the data is a promise, giving us more control, such as waiting for multiple requests to complete:

const ProjectPage = props => {
  const { fetchData, fetchOtherData, result, project } = props
  useEffect(() => {
    Promise.all([fetchData(), fetchOtherData()]).then(res => {
      // do something
    })
  }, [])
  return <div>
    <h1>{ result }</h1>
  </div>
}

Copy the code

Now that the middleware has been modified to control the initiation of asynchronous requests, combining the scenario we encountered with the solution above, we just need to integrate the logic implemented above into the middleware:

function reqTimeControl({ dispatch, getState }) {
  return next => {
    return action => {
      if (typeof action === 'function') {
        let result = action(dispatch, getState);
        if (result) {
          if ('FETCH' inResult) {const {FETCH} = result const thisTime = date.now ()type: FETCH. Types [0]} result = FETCH (FETCH. Url).then(res => res.json()).then(res => {FETCH. Types [0]}) Const {reqStartLastTime} = getState()if (thisTime > reqStartLastTime) {
                next({
                  type: FETCH.types[1],
                  payload: res
                })
                return res
              }
            }).catch(error => {
              next({
                type: FETCH.types[2],
                payload: error,
              })
              returnError}).finally(() => {const {reqStartLastTime} = getState() const {reqStartLastTime} = getState()if (thisTime > reqStartLastTime) {
                next({
                  type: '__RECORD_REQUEST_START_POINT__',
                  payload: thisTime
                })
              }
            })
          }
        }
        return result
      }
      return next(action)
    }
  }
}

export default reqTimeControl
Copy the code

Note that since it is in middleware, the current time can be retrieved directly, so it does not need to record the time of each request in the store, thus eliminating this step:

  next({
    type: '__RECORD_REQUEST_THIS_TIME__',
    payload: thisTime
  })
Copy the code

It’s still going to be the same, so I’m not going to draw it.

By now, you should have achieved a satisfactory result of abstracting the repetitive logic of maintenance time and deciding whether or not to store data into the middleware, just caring about the business code, and can handle almost any such scenario.

conclusion

This time the repetitive logic of a simple problem is abstracted to middleware, and the race problem of the request is solved with Redux. At the same time, a simple request interceptor is implemented with middleware, which can add token and handle loading state. The example in the article only achieves a general effect. In fact, fetch in the middleware needs to be encapsulated to deal with most business scenarios, such as various request method, body and header parameters. In addition, if you want to understand the principle of middleware, prepared for you an article: a brief review of Redux source code and operation mechanism

If you want to know more about my technical articles, you can follow the public account: a mouthful of front-end