Redux Saga

Introduction

In this part of the tutorial, we will discuss the Redux Saga and its usage in React applications.

The main purpose of the Redux Saga, which acts as middleware, is to take dispatched actions, of specific types, and execute requests to the server, and receive responses.

This tutorial is based on prior knowledge of Actions and their complete flow. If you do not possess this knowledge please refer to this tutorial, before you continue.

In this tutorial, we send JSON objects as requests, and retrieve JSON objects as responses, using http protocol. For binary data transfer, please refer to .

Sagas Flow

In this example, we will have a look at saving a recipe information in the server database action.

This action is used to upload recipe contents as JSON string from the client to the server and saving them in its database.

We define the complete action flow:

  1. Execute event handler function for the event.
  2. Dispatching the action from the event handler function.
  3. Saga middleware will execute the task following the recevied action.
  4. Saga middleware will dispatch an action for the Reducer in cases of success or failure.
  5. Reducer will handle the success and failure action cases.

Flow Example

In our case, we will have a look at the SaveRecipe action that is dispatched once the user clicks on the Save button inside AddRecipe.

The flow is implemented as follows:

  1. Execute ,onClickSaveRecipe, the event handler function, for onClick event on the Save button in AddRecipe component.
  2. Dispatch saveRecipe action from onClickSaveRecipe event handler, in AddRecipePage.
  3. Execute saveRecipe saga in Sagas files. The saga will dispatch Success or Failure actions following the saga result.
  4. The Reducer handles both cases resulted from executing the saga function. This will trigger view update due to state change of the redux store.

Event Handler

In the code below, we execute onClickSaveRecipe event handler, for the onClick event of the Save button found in AddRecipe file. We provide the event handler function the recipe contents as JSON object taken from AddRecipe props.

The addRecipePageState object is retrieved from the redux store of the application. The code to retrieve this recipe is found in mapStateToProps function of the AddRecipePage container file.

In [ ]:
const AddRecipe = (props) => {
  return (
    <Button
      className={props.classes.rightIcon}
      variant="contained"
      color="secondary"
      onClick={() => {
        props.onClickSaveRecipe(props.addRecipePageState);
      }}>
      <Save className={props.classes.leftIcon}/>
      Save
    </Button>
  )
};

Action Dispatch

We now implement the onClickSaveRecipe function which will dispatch the saveRecipe action. The code below is part of the mapDispatchToProps arrow function found under AddRecipePage file.

In [ ]:
const mapDispatchToProps = (dispatch) => {
 return {
    onClickSaveRecipe: (recipe) => {
      dispatch(recipeActions.saveRecipe(recipe))
    },
  }
};

Action Implementation

We need to implement both the saveRecipe action, and the SAVE_RECIPE action constant. The action is found under actions file, and the relevant constants are found under the constants file.

The saveRecipe contains the following fields:

  1. type - which is used by the Saga to handle it.
  2. url - denotes the url path. The url is used by the server to route it to the correct handling function.
  3. payload - contains the JSON object of the recipe.
In [ ]:
function saveRecipe(recipe){
  return {
    type: recipeConstants.SAVE_RECIPE,
    url: '/api/save/recipe',
    payload: recipe
  }
}

Saga Implementation

Two things are required:

  • Implementing the generator function which will be executed once for each action dispatched to the saga
  • Notifying the Saga to take every (or take latest) action dispatched.

Implementing saveRecipe

To implement a saga, and execute a server request, we call a fetch command and provide it:

  • server url - this allows proper routing at the sever to correct handling function.
  • http request contents - method, headers, and body.
    • method: using POST in this example to send the server data.
    • headers: defining the data type as json.
    • body: which will contain the recipe data, after converting them from JSON object to JSON string using JSON.stringify Once the request is sent, we want to retrieve a response, so we call on the result of the request, and define the data type we are expecting to receive, which in this case is json.

If the request succeeds and we receive a response, we dispatch a new action called SAVE_RECIPE_SUCCESS with the received json payload. If the request fails for any reason we are trown to the catch clause. We dispatch a SAVE_RECIPE_FAILURE action containing the error message.

Both of these cases are handled by the reducer.

In [ ]:
function* saveRecipe(action) {
  try {
    const res = yield call(fetch, action.url,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(action.payload)
      });

    const json = yield call([res, 'json']); //retrieve body of response
    yield put({type: recipeConstants.SAVE_RECIPE_SUCCESS, payload: json});
  } catch (e) {
    yield put({type: recipeConstants.SAVE_RECIPE_FAILURE, message: e.message});
  }
}

Saga Definition

Once we are done with defining the saga, we define how the saga is handled. We can takeEvery or takeLatest. In our case we will be using takeEvery which tells Redux to handle every action dispatched of this type.

The code below defines the saga to handle every SAVE_RECIPE dispatched.

In [ ]:
function* AddRecipePageSagas() {
  yield takeEvery(recipeConstants.SAVE_RECIPE, saveRecipe);
}

export default AddRecipePageSagas;

Reducer Implementation

Once the action is handled by the saga, two possible outcome actions may be dispatched:

  1. SAVE_RECIPE_SUCCESS - then we update our state with lastSaved date stamp, and its id.
  2. SAVE_RECIPE_FAILURE - then we print an error message.

We add those two cases to our reducer for proper handling.

In [ ]:
const addRecipePageReducer = (state = initialState, action) => {
  switch (action.type) {
    case recipeConstants.SAVE_RECIPE_SUCCESS:
      return state
          .set('lastSaved', fromJS(action.payload.lastSaved))
          .set('_id', fromJS(action.payload._id));
    case recipeConstants.SAVE_RECIPE_FAILURE:
      console.log(recipeConstants.SAVE_RECIPE_FAILURE + ' reducer');
      return state;
  }
}

Final Words

This tutorial provided the basic details of the following elements:

  1. Redux saga flow
  2. Saga implementation example
  3. Http server request
  4. Handling saga dispatched actions at the reducers.

Created: July 30th, 2018 Updated: July 30th, 2018