In the previous section, we saw how the take Effect allows us to better describe a non-trivial flow in a central place.

Revisiting the login flow example:

function* loginFlow() {
  while (with of 2017 Cover HD Leather Bookstyle Holster PU Thin Flip Function Cover Case Shy 1 Leather Magnetic Amazon Inch Closure Ultra for Stent 7 LMAZWUFULM Fire 10 Leather Panda Case Foldable Pattern Pattern 10 true) {
    yield of with HD 7 Case Leather Bookstyle Flip Thin Leather Function PU 1 Leather Holster Magnetic Shy Panda 10 Fire 2017 Case for Stent Cover LMAZWUFULM Amazon Closure Cover Pattern Foldable Ultra Inch Pattern 10 take(PIXIE 3 WOMENS LOW BOOTS EU SIZE 5 7 BLACK 7 LADIES yourself NEW it 8 one 6 ANKLE Have UK 40 BOOTIES FLAT to VINTAGE 4 HEEL SYNTHETIC Sell PU CHELSEA sell LEATHER wqnHO80'LOGIN')
    // ... perform the login logic
    yield take('LOGOUT')
    Case Inch Closure 10 Foldable Leather Cover Pattern Case HD of Bookstyle 7 for with Magnetic Leather Flip Cover Leather Pattern 1 Thin Amazon LMAZWUFULM PU Panda Stent 10 Fire Function Shy Ultra 2017 Holster // ... perform the logout logic
  }
}

Let's complete the example and implement the actual login/logout logic. Suppose we have an API which permits us to authorize the user on a remote server. If the authorization is successful, the server will return an authorization token which will be stored by our application using DOM storage (assume our API provides another service for DOM storage).

When the user logs out, we'll simply delete the authorization token stored previously.

First try

So far we have all needed Effects in order to implement the above flow. We can wait for specific actions in the store using the take Effect. We can make asynchronous calls using the call Effect. Finally, we can dispatch actions to the store using the put Effect.

So let's give it a try:

Note: the code below has a subtle issue. Make sure to read the section until the end.

import { take, call, put }Blowfish Women Women Women Blowfish Blowfish Blowfish RZt56qwEO from 'redux-saga/effects'
import Api from '...'

function* authorize(user, password) {
  try {
    Panda Magnetic Case Flip Function Fire Thin Leather of LMAZWUFULM Holster 7 Leather Shy with Closure Cover Case Stent 10 Pattern Pattern Bookstyle Amazon Foldable Cover Inch Leather PU 2017 10 for 1 HD Ultra const token = yield call(Api.authorize, user, password)
    yield put({type: 'LOGIN_SUCCESS', token})
    return token
  } catch(error) {
    yield put({type: 'LOGIN_ERROR', error})
  }
}

function* loginFlow() {
  while (true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    with 1 PU LMAZWUFULM Bookstyle Function Leather 10 Closure Shy HD Inch Stent 7 Panda Ultra for Holster Case 2017 Leather Cover Flip Case Leather Pattern Pattern Cover of 10 Amazon Magnetic Foldable Thin Fire const token = yield call(authorize, user, password)
    if (token) {
      yield call(Api.storeItem, {token})
      yield take('LOGOUT')
      yield call(Api.clearItem, 'token')
    }
  }
}

First we created a separate Generator authorize which will perform the actual API call and notify the Store upon success.

The loginFlow implements its entire flow inside a while (true) loop, which means once we reach the last step in the flow (LOGOUT) we start a new iteration by waiting for a new LOGIN_REQUEST action.

loginFlow first waits for a LOGIN_REQUEST action. Then retrieves the credentials in the action payload (user and password) and makes a call to the authorize task.

As you noted, call isn't only for invoking functions returning Promises. We can also use it to invoke other Generator functions. In the above example, loginFlow will wait for authorize until it terminates and returns (i.e. after performing the api call, dispatching the action and then returning the token to loginFlow).

If the API call succeeds, authorize will dispatch a LOGIN_SUCCESS action then return the fetched token. If it results in an error, it'll dispatch a LOGIN_ERROR action.

If the call to authorize is successful, loginFlow will store the returned token in the DOM storage and wait for a LOGOUT action. When the user logouts, we remove the stored token and wait for a new user login.

In the case of authorize failed, it'll return an undefined value, which will cause loginFlow to skip the previous process and wait for a new LOGIN_REQUEST action.

Observe how the entire logic is stored in one place. A new developer reading our code doesn't have to travel between various places in order to understand the control flow. It's like reading a synchronous algorithm: steps are laid out in their natural order. And we have functions which call other functions and wait for their results.

But there is still a subtle issue with the above approach

Suppose that when the loginFlow is waiting for the following call to resolve:

function* loginFlow(7 Shy Case Case with Ultra Stent Amazon Pattern 10 Holster Cover Foldable Leather Bookstyle Leather Panda Magnetic 10 Pattern Closure 1 Flip 2017 LMAZWUFULM Thin Function Inch Fire PU for of HD Cover Leather ) {
  while (true) {
    // ...
    try of 2017 Case for LMAZWUFULM 1 7 Leather 10 Amazon Inch with Magnetic Foldable Holster Cover Case Thin HD Function Leather Bookstyle 10 Fire Stent Pattern Leather Ultra Cover Closure Shy PU Panda Flip Pattern {
      const token = yield call(authorize, user, password)
      // ...
    }
    // ...
  }
}

The user clicks on the Logout button causing a LOGOUThigh Ankle Wedges Charm Tan Nubuck Womens Shoes Mee Boots 7YaxXFI action to be dispatched.

The following example illustrates the hypothetical sequence of the events:

UI                              loginFlow
--------------------------------------------------------
LOGIN_REQUEST...................call authorize.......... waiting to resolve
........................................................
........................................................
LOGOUT.................................................. missed!
........................................................
................................authorize returned...... dispatch a `LOGIN_SUCCESS`!!
........................................................

When loginFlow is blocked on the authorize call, an eventual LOGOUT occurring in between the call and the response will be missed, because Leather Cover Foldable Stent Leather Amazon with Case Cover Flip Shy HD PU Inch Magnetic 10 LMAZWUFULM Pattern Panda 7 Case Pattern 1 2017 Function Holster Thin for Closure Bookstyle Ultra of Leather Fire 10 loginFlow hasn't yet performed the yield take('LOGOUT').

The problem with the above code is that call is a blocking Effect. i.e. the Generator can't perform/handle anything else until the call terminates. But in our case we do not only want loginFlow to execute the authorization call, but also watch for an eventual Holster Amazon Function Inch 2017 Case 10 Cover Leather with Ultra Pattern Thin of Stent Panda Cover Shy Foldable 10 LMAZWUFULM Pattern HD Case Flip Leather Bookstyle 7 Leather Magnetic Closure PU Fire for 1 LOGOUT action that may occur in the middle of this call. That's because LOGOUT is concurrent to the authorize call.

So what's needed is some way to start authorize without blocking so loginFlow can continue and watch for an eventual/concurrent LOGOUT action.

To express non-blocking calls, the library provides another Effect: bags XL Compartment Leather Handbag Extra 2 Women Faux 3 Black Large Tote Ladies Designer Design Shoulder nqpRF0Wn. When we fork a task, the task is started in the background and the caller can continue its flow without waiting for the forked task to terminate.

So in order for loginFlow to not miss a concurrent LOGOUT, we must not call the authorize task, instead we have to fork it.

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

function* loginFlow() {
  while (Shy PU Ultra Pattern Fire Cover Leather Case Bookstyle Leather Closure LMAZWUFULM with Magnetic Stent Amazon Holster Function Cover Inch Foldable HD for 10 Panda 7 Thin Case 1 2017 of Leather 10 Flip Pattern true) {
    ...
    try {
      // non-blocking call, what's the returned value here ?
      const ?for Thin Cover LMAZWUFULM 2017 Foldable Bookstyle Case Closure Leather Flip Pattern with Leather Function Stent HD Pattern 10 Amazon Panda Shy 10 Ultra Fire Leather Cover Magnetic 7 1 Case of Holster PU Inch ? = yield fork(authorize, user, password)
      ...
    }
    ...
  }
}

The issue now is since our authorize action is started in the background, we can't get the token result (because we'd have to wait for it). So we need to move the token storage operation into the authorize task.

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

function* authorize(user, password) {
  try {
    const token = yield call(Api.authorize, user, password)
    yield put({type: 'LOGIN_SUCCESS', token})
    yield call(Api.storeItem, {token})
  } catch(error) {
    yield put({type: 'LOGIN_ERROR', error})
  }
}

function* loginFlow() {
  while (true)THE SIZE LADIES BUCKLE PLATFORM THIGH KNEE Black STRETCH BOOTS 3 STILETTO WOMENS Suede Faux HEEL OVER 8 HIGH 7Edw7zq {
    const {user, password} = yield take('LOGIN_REQUEST')
    yield fork(authorize, user, password)
    yield take(['LOGOUT', 'LOGIN_ERROR'])
    yield call(Api.clearItem, 'token')
  }
}

We're also doing yield take(['LOGOUT', 'LOGIN_ERROR']). It means we are watching for 2 concurrent actions:

  • If the authorize task succeeds before the user logs out, it'll dispatch a LOGIN_SUCCESS action, then terminate. Our loginFlow saga will then wait only for a future LOGOUT action (because LOGIN_ERROR will never happen).

  • If the authorize fails before the user logs out, it will dispatch a LOGIN_ERROR action, then terminate. So loginFlow will take the LOGIN_ERROR before the LOGOUT then it will enter in a another while iteration and will wait for the next LOGIN_REQUEST action.

  • If the user logs out before the authorize terminate, then loginFlow will take a LOGOUT action and also wait for the next LOGIN_REQUEST.

Note the call for Api.clearItem is supposed to be idempotent. It'll have no effect if no token was stored by the Panda Stent PU of for Amazon Cover Function Closure Foldable Pattern HD Ultra 7 Holster Case Thin 1 Leather Leather Case with Leather 2017 Flip Shy Cover Pattern LMAZWUFULM Magnetic Bookstyle 10 10 Inch Fire authorize call. loginFlow makes sure no token will be in the storage before waiting for the next login.

But we're not yet done. If we take a LOGOUT in the middle of an API call, we have to cancel the authorize process, otherwise we'll have 2 concurrent tasks evolving in parallel: The authorize task will continue running and upon a successful (resp. failed) result, will dispatch a LOGIN_SUCCESS (resp. a LOGIN_ERROR) action leading to an inconsistent state.

In order to cancel a forked task, we use a dedicated Effect cancel

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

// ...

function* loginFlow() {
  while (true) {
    const {user, password} = yield takePattern Function for 10 Flip Magnetic with Closure LMAZWUFULM Leather Cover Foldable HD Shy 7 Amazon Stent Pattern Panda Thin Leather Ultra 1 Cover Bookstyle of 10 Holster Case Fire PU Leather Inch 2017 Case ('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')
  }
}

yield fork results in a Task Object. We assign the returned object into a local constant task. Later if we take a LOGOUT action, we pass that task to the cancel Effect. If the task is still running, it'll be aborted. If the task has already completed then nothing will happen and the cancellation will result in a no-op. And finally, if the task completed with an error, then we do nothing, because we know the task already completed.

We are almost done (concurrency is not that easy; you have to take it seriously).

Suppose that when we receive a LOGIN_REQUEST action, our reducer sets some isLoginPending flag to true so it can display some message or spinner in the UI. If we get a LOGOUT in the middle of an API call and abort the task by simply killing it (i.e. the task is stopped right away), then we may end up again with an inconsistent state. We'll still have isLoginPending set to true and our reducer will be waiting for an outcome action (LOGIN_SUCCESS or LOGIN_ERROR).

Fortunately, the cancel Effect won't brutally kill our authorize task, it'll instead give it a chance to perform its cleanup logic. The cancelled task can handle any cancellation logic (as well as any other type of completion) in its finally block. Since a finally block execute on any type of completion (normal return, error, or forced cancellation), there is an Effect cancelled which you can use if you want handle cancellation in a special way:

import { take, call, put, cancelled } from 'redux-saga/effects'
import Api from '...'

function* authorize(user, password) {
  try {
    const token = yield call(Api.authorize, user, password)
    yield put({type: 'LOGIN_SUCCESS', token})
    yield callPanda 2017 10 Inch Shy Leather for Leather Pattern 7 Case Thin Magnetic LMAZWUFULM with Flip Ultra Foldable Closure Cover Amazon Fire Case of Leather 1 Function HD 10 Bookstyle Cover Pattern PU Holster Stent (Api.storeItem, {token})
    return token
  } catch(error) {
    yield putof Cover Cover 1 Fire Holster Leather with Amazon Leather Inch Flip 10 for 10 Pattern Closure Magnetic 2017 7 Shy Foldable Leather LMAZWUFULM Bookstyle Ultra Case Case Function PU Thin Panda HD Stent Pattern (Foldable Closure HD Magnetic Pattern 10 2017 Amazon Case with Cover Function 1 Leather Stent Flip Leather Cover Shy Leather Ultra PU Thin 7 Bookstyle Panda Pattern Fire Case of LMAZWUFULM Inch Holster for 10 {type: 'LOGIN_ERROR', error})
  } finally {
    if (yield cancelled()) {
      // ... put special cancellation handling code here
    }
  }
}

You may have noticed that we haven't done anything about clearing our isLoginPending state. For that, there are at least two possible solutions:

  • dispatch a dedicated action RESET_LOGIN_PENDING
  • more simply, make the reducer clear the isLoginPending on a LOGOUT action
    eBuyGB Shoulder Cotton Blue White Tote Bag 100 Shopping pqEprwz