In the analysis of the principle of realizing asynchronous effect in Dva, we know that the bottom layer of Dva encapsulates Redux-Saga and uses Redux-Saga to realize asynchronous effect. This chapter will briefly explain the origins of Redux-Saga, how to start Redux-Saga, and some of the REdux-Saga apis.

The origin of

Redux, as a state management repository, plays a very important role in our front-end application. Here is a picture of the official Redux flow:In the workflow of Redux, the user distributes the action, and the Store automatically calls Reducer after receiving the action and passes in two parameters: the current State and the received action. The Reducer returns the new State.

We know that data flows in REdux are synchronous and there is no support for asynchronous actions to update or retrieve data, but asynchronous requests for data are definitely common in real projects, hence the birth of Redux middleWare. The middleware can perform actions with side effects between issuing actions and receiving actions by the Reducer function.

Redux-thunk and Redux-Saga are definitely the two most popular redux middleware today. The main idea of redux-Thunk is to extend an action from an object to a function that can handle side effects. However, Redux-Saga still uses ordinary actions. The central idea of Redux-Saga is to intercept the sent actions and then handle the side effects. It has its own set of logic to control the asynchronous flow.

How Saga works

Redux-saga implements asynchronous Effects relying on es6 Generator features. Saga usually yields an Effect (that is, a Plain Object JavaScript Object), and using the yield keyword suspends the execution of a function until the code following yield completes.

An Effect is an object that contains instructions to be performed by Saga Middleware. We can use the factory functions provided by Redux-Saga (Call, fork, PUT, take, etc.) to create effects. For example, we can use call(myfunc, ‘arg1’, ‘arg2’) to instruct middleware to call myfunc(‘arg1’, ‘arg2’) and return the result to the yield Effect Generator. You can think of effects as instructions sent to middleware to perform some action (call some asynchronous function, initiate an action to a store, etc.).

Sagas consists of three main parts for joint task execution:

  • worker saga

Handles all asynchronous operations, such as calling apis, making asynchronous requests, and getting results back.

  • watcher saga

Listen for actions that are dispatched and call worker Saga to perform the task when an action is received or known to be triggered.

  • root saga

The only way to start Sagas immediately is generally to introduce Saga’s middleware in our project file entry and start Saga.

We usually put worker Saga and Watcher Saga together in a sag.js file that handles all side effects, expressing sagas logic.

Start the sagas

First we create a sagas.js file and add the following code snippet:

export function* helloSaga() { console.log('Hello Sagas! '); }Copy the code

To run our Saga, we need:

  • Create a Saga Middleware and a Sagas to run (such as helloSaga we created)
  • Connect the Saga Middleware to the Redux store.

Next, we add the following code to the entry file:

import { createStore, applyMiddleware } from 'redux' import createSagaMiddleware from 'redux-saga' import { helloSaga } from './sagas' const sagaMiddleware = createSagaMiddleware(helloSaga); const middlewares = [sagaMiddleware]; const store = createStore(reducer, applyMiddleware(... middlewares)); sagaMiddleware.run(helloSaga);Copy the code

First we introduced helloSaga, the root Saga written above, and created a SagaMiddleware using the Redux-Saga module createSagaMiddleware. We then use applyMiddleware to connect Saga Middleware to the Store. Then run Saga using sagamiddleware.run (helloSaga).

The sagas.js file is the heart of saga, where we can write Watcher Saga to listen for a specific or all action and execute the corresponding worker saga (handling asynchrony, business logic, etc.). Next, we’ll look at some of the apis provided by Redux-Saga to help us handle these actions better.

Effect creator

As mentioned earlier, an Effect is a text object that contains instructions, and Redux-Saga/Effects provides several ways to create an Effect.

call(fn, … args)

Create an Effect description that commands middleware to call fn with args.

function* fetchProducts(dispatch)
  const products = yield call(Api.fetch, '/products')
  dispatch({ type: 'PRODUCTS_RECEIVED', products })
}
Copy the code

Fetch tells middleware to execute api. fetch and pass in ‘/products’, assign the result returned to Products, and initiate a PRODUCTS_RECEIVED action. Because call creates a blocking task, the Generator will wait until middleware completes api.fetch (‘/products’) and returns before continuing with the dispatch statement below.

fork(fn, … args)

Sagas also provides a way to create non-blocking tasks — fork creates an Effect description that commands middleware to execute FN as a non-blocking call. Returns a Task object that can be used to cancel the corresponding branch Task.

function* fetchProducts(dispatch)
  const products = yield fork(Api.fetch, '/products')
  dispatch({ type: 'PRODUCTS_RECEIVED', products })
}
Copy the code

Similarly, after fork(api.fetch, ‘/products’), the Generator won’t be paused while waiting for fn to return the result; Instead, it resumes execution as soon as FN is called. Fork is better than Call when we don’t want some asynchronous operation to block the autonomous process.

take(pattern)

Creates an Effect description that commands middleware to wait for the specified action on the Store. The Generator pauses until an action matching pattern is initiated.

  • If PATTERN is empty or *, all initiated actions will be matched.
  • If pattern is a function, actions whose pattern(action) is true will be matched. (for example, take(action => action.entities) will match which entities fields are true actions)
  • If pattern is a string, then action. Type === action of pattern will be matched.
  • If it is an array, then each item in the array applies to the above rule, and any item in the array that is matched captures the corresponding action.
function* loginFlow() {
  while (true) {
    yield take('LOGIN')
    // ... perform the login logic
    yield take('LOGOUT')
    // ... perform the logout logic
  }
}
Copy the code

For example, we can handle some logic when listening to an action ‘LOGIN’ and other logic when listening to an action ‘LOGOUT’, which is always paired with ‘LOGOUT’.

cancel(task)

Creates an Effect description that commands middleware to cancel a previous fork task.

import { take, put, call, fork, cancel } from 'redux-saga/effects'

function* loginFlow() {
  while(true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    // fork return a Task object
    const task = yield fork(authorize, user, password)
    const action = yield take(['LOGOUT', 'LOGIN_ERROR'])
    if(action.type === 'LOGOUT')
      yield cancel(task)
    yield call(Api.clearItem('token'))
  }
}
Copy the code

In the above code, we listen for a future LOGIN_REQUEST action. At some point in the future, when the action is initiated, the Generator resumes execution. After receiving the user and password, Verify the validity of the user through the authorize function. We used fork instead of call because we didn’t want to miss the LOGOUT or LOGIN_REQUEST action while verifying that the user was legitimate, so we chose a non-blocking fork instead of a blocking call. At the same time, fork returns a Task, and if we listen to the LOGOUT or LOGIN_REQUEST action, i.e. the user LOGOUT or login error occurs, we will cancel the branch Task to verify that the user is valid. If the branch task is not completed when the corresponding action is heard, the cancel operation will cancel the task. If the branch task has finished at this point, the Cancel operation does nothing.

Saga auxiliary function

In addition to the take operation, Redux-Saga provides a number of helper functions to help us better intercept actions.

takeEvery(pattern, saga, … args)

Spawn a saga on every action that dispatches to a Store and matches the pattern.

import { takeEvery } from `redux-saga/effects`

function* fetchUser(action) {
  ...
}

function* watchFetchUser() {
  yield takeEvery('USER_REQUESTED', fetchUser)
}

Copy the code

We created a simple task called fetchUser. We use takeEvery to start a new fetchUser task each time a USER_REQUESTED action is initiated.

TakeEvery is a high-level API built using take and fork.

const takeEvery = (pattern, saga, ... args) => fork(function*() { while (true) { const action = yield take(pattern) yield fork(saga, ... args.concat(action)) } })Copy the code

TakeEvery allows concurrent actions to be processed (that is, the same action is triggered at the same time).

takeLatest(pattern, saga, … args)

Derive a saga on every action that is initiated to the Store and matches the pattern, and automatically cancel all saga tasks that have been started but are still in progress. TakeLatest, like takeEvery, is a high-level API built using take and fork, but takeLatest only handles the latest action that is triggered.

const takeLatest = (patternOrChannel, saga, ... args) => fork(function*() { let lastTask while (true) { const action = yield take(patternOrChannel) if (lastTask) { } lastTask = yield fork(saga,... args.concat(action)) } })Copy the code

conclusion

Redux-saga and Redux-Thunk solve the same problem, but the implementation is different. Saga puts all the operations of the asynchronous request in the saga file, while Thunk just performs some operations on the original action (if the action is a function, it will execute that function). Redux-saga offers more apis, advanced asynchronous control flow, and concurrency management than Redux-Thunk, making it more suitable for large and complex projects, but also more expensive to learn.