Redux-saga has a high threshold to get started, which is unfavorable for the promotion of Redux-Saga within the team. For newcomers, there is no convincing reason to give up redux-Thunk or async/await scheme in the official materials of Redux-Saga. This article is a study note summarized in my learning process and understanding of the project. By analyzing the functions of Redux and comparing the solutions to deal with side effects in Redux, it explains what Redux-Saga is, what it is used for and the realization principle.

background

redux

Redux reduces maintenance for large projects. There is a common approach to decoupling in object orientation: “interface and implementation separation”. By defining an interface, the class developer only needs to expose the interface to the business, who only needs to know what behavior the interface defines, not the implementation behind it, and the class author only needs to ensure that the corresponding interface is implemented when updating. The epitome of this pattern is dependency injection (like Spring and Angular). Redux is very similar to this pattern: If the interface is understood as action and the specific implementation is reducer, the business party can only operate the state of the whole store through action. For the business party, it does not need to know the specific implementation of the Reducer, and it has no sense for the business party to update the Reducer. The Redux programming pattern is another form of “interface and implementation separation.”

The advantages of “interface and implementation separation” are familiar to you and will not be discussed here.

The model is not flawless, however. Redux is criticized for its large amount of code, which at first writing is not as fast as processing business logic directly in components, so redux is not suitable for small or quick trial-and-error projects. The community has proposed several solutions to mitigate this shortcoming, such as Redux-Action.

Side effects

No side effects means that a function has no additional effects on the caller other than the return value, such as modifying function input parameters, reading or modifying global environment variables, writing IO, and reading IO. The advantages of a side-effect free function have been made clear here.

Reducer is required to have no side effects in the official redux tutorial. Why is that? The following two pieces of code add 1 to all elements of an array, strip the array of all elements greater than 10, and return the array.

function transformArr(arr) {
   // There are side effects
   for (let i = 0; i < arr.length; i++) {
        arr[i] += 1;
        if(arr [I] >10) {
            arr.splice(i, 1); }}return arr;
}

function transformArr(arr) {
   // No side effects
   const greaterThen10Arr = arr.map(e= > e + 1).filter(e= > e > 10);
   return greaterThen10Arr;
}
Copy the code

As you can see, code with no side effects is a much clearer way of expressing the author’s intent. Imagine if you saw a for loop in someone else’s code that iterated through an array, and you saw what the loop did to the array, and it was more frightening to see if the loop did anything other than the array. The code with no side effects just needs to look at the variable name to see what kind of array the author wants. Similarly, if there are adverse effects on a Reducer, you will most likely have to go back to see what the reducer did and whether it affected the new capabilities. This goes against the redux concept of separating interface and implementation.

Side effect Management plan

When Redux was born, the JS community was still debating how to handle asyncracy. Probably because of this, Redux didn’t have a solution for side effects. However, the front-end can’t make all code free of side effects. Here is the community’s plan.

async/await

This is the simplest solution for small projects, such as the following code, after obtaining the user ID, obtain the corresponding data according to the user ID, and finally issue an action to save the data to the store.

function fetchData(userId) {
    // Return a promise containing data
}

function fetchUser() {
    // Return a promise with user information
}

class Component {
    componentDidMount() {
        const { userInfo: { userId } } = await fetchUser();
        store.dispatch({type: 'UPDATE_USER_ID', payload: userId});
        const { data } = await fetchData(userId);
        store.dispatch({type: 'UPDATE_DATA', payload: data}); }}Copy the code

What if other components need to reuse this logic? A better solution is to create a class that does this:

class DataHandler {
    static fetchData() {
        const { userInfo: { userId } } = await fetchUser();
        store.dispatch({type: 'UPDATE_USER_ID', payload: userId});
        const { data } = await fetchData(userId);
        store.dispatch({type: 'UPDATE_DATA', payload: data}); }}classComponentA { componentDidMount() { DataHandler.fetchData(); }}classComponentB { componentDidMount() { DataHandler.fetchData(); }}Copy the code

But this leads to a maintenance problem: How can the DataHandler be extended? For example, If ComponentB wants to transform the data in the fifth row at the top and update the other fields in the store after the network request is over, you might need to add a special function to the DataHandler for B. Over time, the DataHandler becomes more and more bloated. The most important thing is that the component is dependent on the DataHandler. Every modification of the DataHandler requires checking all the components that reference the DataHandler, and the DataHandler becomes increasingly unmaintainable. Because we need redux.

redux-thunk

The redux-Thunk usage is not here, the main advantages are flexible and easy to use, low team promotion difficulty, overcome the disadvantages of DataHandler, ideal for small projects.

The disadvantage is that there is no fine-grained control over asynchrony (described below) and it is not easy to test. It should be said that this article cannot and will not address the absolute advantages of Redux-Saga over Redux-Thunk, which one to use is entirely up to the scenario and the team.

redux-saga

The biggest disadvantages of Redux-Saga are the absolute high barrier to understanding and lack of usage scenarios that can be openly discussed —- small projects are useless, and large projects are not necessarily suitable for discussing specific technical details in public, which results in a lack of learning resources —- except API use.

Redux-saga is a very powerful tool for dealing with side effects. It provides more fine-grained control over asynchronous processes, with pause, stop, and start states for each asynchronous process. In addition, Redux-Saga makes use of the Generator, and the testing method can be very simple for each saga. More importantly, the asynchronous processing logic is in saga, so we can listen for action triggers and have side effects. Action is still a normal Redux action, without breaking redux’s definition of action.

How does Redux-Saga work

Redux-saga is based on generators. I’m sure we all know what a generator is, but the concept is vague and confusing. Let’s pull a random generator example:

function* gen() {
  const x = yield 1;
  const y = yield 2;
}
const g = gen();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
Copy the code

This is a very simple example of a generator, but we still don’t know what a generator can do. In fact, we can think of Gen as a to-do list, and then one person gets the to-do list and starts doing the tasks. For example:

function* gen() {
    const user = {userName: 'xiaoming'};
    const article = {articles: [{title: 'articles'}};const x = yield Promise.resolve(user);
    console.assert(x === user); // true
    const y = yield Promise.resolve(article);
    console.assert(y === article); // true
  }
  
 async function worker(gen) {
    const g = gen();
    let task = g.next();
    while(! task.done) {const response = await task.value;
        task = g.next(response);
    }
}

worker(gen);
Copy the code

The worker is the person performing the task, and Gen is the task list. If you think of Redux-Saga, redux-Saga is the worker, all we need to do is write a task list —– write a saga, what is saga?

What is the saga

Saga is a strange name, here is the dictionary and paper definition:

A saga is A long story, account, or sequence of events. Thesis: A LLT(Long Lived Transactions) is A sagaif it can be written as a sequence of transactions that can be interleaved with other transactions.
Copy the code

A simple explanation is that a saga is a list of tasks. The tasks are executed in an orderly order, and the status of each task can be changed. Order is easy to understand, but what can be changed? The following code is the code for the project’s network request:

function* request(action: PayloadAction) {
  try {
    yield put(requestActions.start(action));
    const response = yield call(apiRequestAndReturnPromise);
    yield put({type: `${action.type}`_SUCCESS, payload: response});
  } catch (error) {
    yield put({type: `${action.type}`_FAIL, payload: error}); }}function* cancelSendRequestOnAction(abortOn: string | string[], task: any) {
  const { abortingAction } = yield race({
    abortingAction: take(abortOn), // Can be an array
    taskFinished: join(task),
    timeout: call(delay, 10000), // taskFinished doesn't work for aborted tasks
  });
  if (abortingAction) {
    yieldcancel(task); }}function* requestWatcher() {
    const newTask = yield fork(apiRequest, action); // Task 1
    yield fork(cancelSendRequestOnAction, abortOn, newTask); // Task 2
}
Copy the code

This code might get a little convoluted, and you might need to look at it a few more times.

First, the requestWatcher starts two asynchronous tasks with a fork. Because of the fork, Task 2 does not wait for Task 1 to finish, but executes it immediately after Task 1 starts.

The newTask returned by fork is a Saga task object that we can process, such as cancel.

Task 1 is making a network request to obtain data. Nothing special.

Task 2 is the focus that explains what a saga’s state can change. AbortingAction (Task2) abortingAction (taskFinished) abortingAction (timeout) abortingAction (abortingAction)

This is an interesting feature. In addition to network requests on the front end, there are asynchronous behaviors such as Promises, setTimeout, setInterval, and click events (welcome to add). Redux-saga makes it easy to implement complex interactions, especially in editors.

conclusion

  • Redux can achieve interface and implementation separation, async/await processing side effects can break this design pattern;
  • Code without side effects is generally of higher quality;
  • There is no better redux-Thunk than Redux-Saga, only the scene;
  • The generator should essentially have a “to-do list” and a “worker”. We give the written “to-do list” to the worker, who is responsible for completing the task list, and Redux-Saga is the worker.
  • A saga is a collection of tasks that are ordered and whose state can be changed by other tasks.

PS: Criticism is warmly welcomed