Today, one of our friends in the group asked a question

Why can’t saga be implemented with async await?

If you are starting to read Redux-Saga, you may wonder why you should write with generator and use async await.

import { put, call } from 'redux-saga/effects'
import { loginService } from '.. /service/request'

function* login(action) {
    try {
        const loginInfo = yield call(loginService, action.account)
        yield put({ type: 'loginSuccess', loginInfo })
    } catch (error) {
        yield put({ type: 'loginFail', error })
    }
}
Copy the code

I also wanted to ask this question when I first started using Saga, but later I understood the principle of saga and figured it out.

Let’s take a look.

Saga principle

We send the action from the component to the store, and the process is synchronous.

But there are some asynchronous processes added where? Middleware.

Redux Saga first passes the action directly to the store. This process is synchronous.

Then it sends a copy to Watcher Saga to check whether it is the monitored action. If it is delivered to Worker Saga for processing, worker Saga can put a new action to store during processing. This process is asynchronous.

This is the principle of Redux-Saga, which is relatively simple in principle, but highlights the organization of asynchronous processes, namely the generator.

Why is the use of generators to organize asynchronous processes the highlight of Redux-Saga?

Let’s take a look at what a generator is.

generator

A generator is a function that produces iterators and returns values by yield.

An iterator, on the other hand, is an object with value and done attributes that are used to traverse a collection sequentially.

We can call iterator.next to yield each value.

You can also use array. from or the expansion operator, which is a characteristic of iterator.

In addition to the next method, iterators also have return and throw methods, just like return and throw statements in functions.

For example, abort the subsequent flow with iterator.return

Throw an error with iterator.throw

That is, the execution of the generator is controlled by an executor, which controls when to yield, when to next, when to return, and when to throw.

The executor can pass the next, return, and throw arguments to the generator as a result of execution:

The code above is a mini-saga, and it’s as simple as that.

So why not async await?

async await

When the generator returns Promise values, then resolve and reject are the only fixed outcomes of a Promise, and it is natural to write a generic executor that automatically calls next, throw, and return.

This is the principle of async await, just syntactic sugar.

Async await is essentially nothing more than an executor of a generator.

If redux-saga is implemented with async await, then all asynchronous logic must be written imperatively after await, which makes the asynchronous process difficult to test. So Redux-Saga implemented an actuator on its own.

Look again at the saga code:

import { put, call } from 'redux-saga/effects'
import { loginService } from '.. /service/request'

function* login(action) {
    try {
        const loginInfo = yield call(loginService, action.account)
        yield put({ type: 'loginSuccess', loginInfo })
    } catch (error) {
        yield put({ type: 'loginFail', error })
    }
}
Copy the code

Instead of promises, effects yield in a generator, which is essentially an object that declaratively tells saga what to do, rather than an imperative implementation.

The executor of the generator thus performs different implementations according to different effects:

Call effect has an implementation and put effect has an implementation, that is, the saga generator executor does not do anything like async await, but has its own runtime.

What’s the good of that?

  • Can replace saga effect specific execution logic, easy to test. Such as switching from request data to a direct return value without even mocking.

  • A series of saga can be built in to facilitate the organization of asynchronous processes such as Throttle, Debounce, Race, and so on

Now, back to the original question, can redux-saga be implemented with async await?

Yes, but async await is an automatic executor with a generator, no runtime logic, it is imperative to write asynchronous processes in saga. If you implement a generator actuator yourself, you can isolate asynchronous processes for easy organization and testing. To implement saga with async await is to lose the soul.

conclusion

The principle of Redux-Saga is to transparently transmit action to store, and then transmit a copy of aciton to the asynchronous process of saga organization. Saga is divided into Watcher Saga and Worker Saga. Watcher Saga determines if the action needs to be handled and then hands it over to Wroker Saga.

A generator is a function that returns an iterator. The iterator has methods like next, throw, and return that need to be executed with an executor.

Async await is essentially an automatic executor of a generator.

Implementing Redux Saga with async await requires the developer to mandate asynchronous processes and is difficult to test.

So Redux-Saga implements a generator executor with its own Runtime. The generator simply returns an Effect object declaratively stating what logic to perform, which is then executed by the corresponding Effect implementation.

In addition to being easy to organize asynchronous processes, this declarative approach is very testable.

The design of the Generator + saga actuator is the soul of redux-Saga and therefore cannot be implemented with async await.