This first post explores redux side effects and Dispatch’s Promiseify source code analysis

Reproduced without permission

We know that Redux’s role is to manage application state so that complex applications can better handle data and map views to data. So how does Redux work? Roughly speaking, it means that user actions generate actions, Dispatch receives actions, and new states are generated after reducer processing. Finally, store and view are updated. See 👇 figure

Side effects

Redux is designed with no side effects, and the state of the data changes can be traced back, so redux is full of pure functions.

What about the side effects? Redux doesn’t offer a straightforward solution. However, it provides a middleware mechanism for users to develop middleware for side effect handling. Many excellent middleware also emerged, such as Redux-Thunk, Redux-Promise, redux-Saga, etc. So how do they deal with side effects? Please see 👇

redux-thunk

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

The most important idea of Redux-Thunk is the Action Creator that accepts a return function. If this Action Creator returns a function, execute it; if not, follow the original next(Action). Because this Action Creator can return a function, you can perform some asynchronous operations in this function.

redux-promise

// Determine if this is a Promise function
import isPromise from 'is-promise';
// Standard Flux action
import { isFSA } from 'flux-standard-action';

export default function promiseMiddleware({ dispatch }) {
  return next= > action => {
    // First check whether the action is the type specified by Flux
    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

Redux-promise Follow through on your promise. Passing the promise as an action to Dispatch and letting the middleware handle resolve eliminates the need to write.then().catch() code. Redux-thunk and redux-Promise are actually quite similar in that they both trigger a function/promise and let the middleware decide when to handle the side effects. This solves most side effects scenarios, but for more complex side effects, a lot of code is required. Redux-saga is a good solution to this problem.

redux-saga

Usage and code will not be discussed. Talking about the concept, Redux-Saga has created a Saga layer specifically designed to handle side effects. So how does Redux-Saga deal with side effects? First, the user behavior generates actions, and when reducer is dispatched, the saga layer listens to specific actions (Redux-Saga provides some auxiliary functions to listen to the actions to be sent to reducer) for processing. The function that handles action is called Effect. Effect is a Generate function that provides yield to control code execution, so redux-saga is suitable for handling asynchracy. In addition, a normal action can continue to be initiated in Effect and be processed by reducer. This is the general implementation of Redux-Saga. Okay, now to the second topic of this article, how to dispatch Promiseify.

dispatch promiseify

Dispatch Promiseify here is implemented using Redux and Redux-Saga. So here we go.

Objective to realize

function* asyncIncrease(action) {
  return action.payload
}
// Omit some steps. store.dispatch({type: 'asyncIncrease'.payload: 30 
}).then(res= > {
  console.log(res); / / 30
})
Copy the code

Since Effect is a function Generate, let’s take a look at some concepts of Generate.

  • Formally,GeneratorA function is an ordinary function, but it has two characteristics. One is that,functionThere is an asterisk between the keyword and the function name; The second is internal use of the function bodyyieldExpression that defines different internal states
  • callGeneratorThe function does not execute and returns not the result of the function’s execution, but a pointer object to the internal state
  • encounteryieldExpression, suspends subsequent operations and will immediately followyieldThe value of the following expression as the value of the returned objectvalueAttribute values.

Okay, that’s it for now, and let’s see how it works. The user initiated the action and sent it to the reducer. Because Reducer is a pure function, that is, the same input always gets the same output without any observable side effects. Therefore, we need to distinguish between actions handled by Redux-saga or reducer. So you have middleware, and you return a promise that enables Dispatch to use the THEN method.

const promiseMiddlware = (a)= > next => action= > {
  // Use type to determine if it is handled by Effect
  // If so, return promise
  if (isEffect(action.type) {
    return new Promise((resolve, reject) = >{ next({ resolve, reject, ... action, }) }) }else {
    return next(action)
  }
}
Copy the code
// Action contains resolve, reject, and payload
function* asyncIncreate(action) {
  const { resolve, payload } = action
  resolve(payload)
}

store.dispatch({ 
  type: 'asyncIncrease'.payload: 30 
}).then(res= > {
  console.log(res); / / 30
})
Copy the code

Implement Promiseify at Dispatch, but don’t you think you need to write resolve every time you write Effect? Then let’s change our thinking. Can we have another Effect specifically to wrap the actual Effect, and the outer Effect to resolve?

// 1. Write the outer Effect first
function* baseEffect(action) {
	const{resolve, reject,... rest} = action// 2. Execute the actual Effect by yield
  const res = yield asyncIncreate(rest)
  resolve(res)
}
// asyncIncreate returns the value ok
function* asyncIncreate(action) {
	return action.payload
}
// omit steps such as createStore
store.dispatch({ 
  type: 'asyncIncrease'.payload: 30 
}).then(res= > {
  console.log(res); / / 30
})
Copy the code

Promiseify at Dispatch is now implemented as a promiseify at Dispatch. This is a rough version of the promiseify at Dispatch, but it can be used in a more flexible way. For more information, go to the DVA source code.

conclusion

Redux is the most popular react state management library. Side effects are a problem due to Redux’s design philosophy, but they are not difficult to solve, and there are good middleware to solve them. If you want to dispatch promiseify with Redux and Redux-Saga, you can write middleware with an outer Effect.