How to create custom saga helpers…

To reduce coding boilerplate

Stefano Susini
Travelex Tech Blog
5 min readNov 9, 2018

--

Photo by Markus Spiske on Unsplash

A rush start

Since the rise of Redux much time has been spent by front-end library designers proposing ways to bridge the gap between the functional (side-effect free), synchronous redux reducer and the omnipresent need of a web application to perform asynchronous work.

Among the many great tools available for the job — such as thunk and redux-observables — redux-sagas has lately gained a lot of attention and has been adopted by many, for the ease of use and test and for how it leverages generators — a powerful new feature of the language introduced in ES7.

When I approached Sagas for the first time I was a bit sceptical and definitely intimidated by the new approach. I couldn’t really understand generators and I also couldn’t easily distinguish the boundaries between the library and the language feature. This can happen to every one of us when rushing into a project or when we need to get up to speed with a well-established team.

The result of this rush is usually a lot of cut and paste from the documentation page that results in an abnormal production of quite a lot of boilerplate code. Let’s see an example of this proliferation in the example described in the next section.

The use case

Let’s say we’re designing an international money transfer application where we need to hit a back-end endpoint to retrieve the rate and show the user the quote of the transaction.

The team has already prepped some code: just the right amount to allow us to start working on the async part.

Back-end interaction utility function
Rate action types and action creators
The rate reducer

They also have created a UI module to handle the loading state of the application along with the page error message:

The previous code should be fairly easy to follow. The two main functions are:

  • Error management: a pair of SET/CLEAR-type actions allow the user to set and clear the page error message.
  • Loading state management: the developer can use this feature to set and reset multiple loading states.
    For example, to set the rate fetching loading state you could write:
    dispatch => dispatch(setLoadingState('FETCH-RATE'))
    To get the current state of the rate fetching loading state (e.g. in the mapStateToProps function):
    state => getLoadingState('FETCH-RATE')(state);

Our job is to create a saga that:

  • Reacts to the FETCH_RATE_START action,
  • Clears the page error,
  • Sets a loading state,
  • Makes the back-end call
  • Handles the HTTP error and
  • Resets the loading state.

Let’s write the test

As a good practice, we will start with a test to describe the behaviour we expect from the saga we are going to define.

In the tests of this article I’m going to use Redux Saga Test Plan: a utility library that automates the steps needed to test the sagas and make the listings much more readable (to beginners, I suggest to start testing your sagas the good old way. This will help you better understand how they work).

The above tests describe the way the sagas should handle the FETCH-RATE-START action in case of a successful outcome or an HTTP error.

First, the saga will clear the page error and set the FETCH-RATE loading state. After that, the saga will try to fetch the rate using getRate; when the call succeeds, the saga dispatches the FETCH-RATE-SUCCES action and reset the loading state.

In case of an HTTP error, the saga will first set the page error (dispatching the SET-ERROR action) and reset the loading state.

The implementation

The following, it’s a plain implementation that would satisfy the test above:

Can we do better? Can we factor out the boilerplate?

As you can see the implementation we’ve come up with, it’s reasonably straightforward and handles all the cases we need it to:

  • The saga will set and reset the loading state; even in case of an error,
  • the getRate function will be called with the right arguments and
  • if an error arise, the saga will set the message in the Redux state

Now imagine your application to be composed by many sagas like this. For example, you’ll have one to search or filter the payments you have in the system, another one is to create a new payment, another one is to review or approve a specific payment, or many payments at the same time… I could continue this list for quite a long time.

You’ll end up repeating a lot of the boilerplate over and over again and you’ll have to test the same behaviour many times.

We would like is to write our saga focusing on the actual business logic and test that. Look at the following snippets:

The test and saga above are simple, straightforward, and only concerned with the logic of fetching the rate. Great! Isn’t it?

Obviously you have noticed something strange at the bottom of the saga file; instead of just using takeLatest, we have introduced the utility we’re going to design and implement to remove the boilerplate: withLoadingAndErrors.

Let’s write a test to define the API and behaviour:

We can start noting that withLoadingAndErrors is not a saga, but is a Saga helper; at lines 16 to 18 we’re using it to create a testSaga to be able to easily test it with redux-saga-test-plan.

At the top of the test we also define a childSaga, used as a placeholder of a real saga, and a loadingState, used as a loading state key.

The first test, "withLoadingAndErrors should automate the LOADING management", shows the behaviour that the helper should implement when the child saga doesn’t throw an error:

  • When the testAction is dispatched — line 20,
  • set the appropriate loading state — line 21,
  • clear the page error — line 22,
  • run the childSaga with the appropriate arguments — line 23
  • and, on completion, reset the loading state — line 24

"withLoadingAndErrors should automate the ERRORS management” describes the error management behaviour:

  • When the testAction is dispatched, the loading state is set and the page error is cleared — lines 29–32
  • if the childSaga throws an Error — lines 33–38
  • set the page error — line 39
  • and reset the loading state — line 40

Write the helper code

As I mentioned before, the helper is not a saga, it’s a function that returns a saga, like takeLatest or takeEvery. The signature of the function will be the following:

const withLoadingAndErrors = 
(loadingKey, saga, ...sagaArgs) => function* (action)

The function take as arguments a loadingKey string and the child saga to run (and the optional sagaArgs to be passed to the child saga when called).

Let’s see how we can implement such a utility function:

All tests pass

As you can see the helper is a simple rearrangement of the basic saga with a little bit of wiring around it. Note that we assume we’re going to receive the dispatched action as input (Line 18) and that we’re passing it as a first argument to the child saga (Line 22).

All the tests pass.

Conclusions

With our helper, we have reduced the initial saga code by 76% and we have turned a hard to read redundant implementation into a simple business logic related — almost stupid — algorithm.

The testing side has also been simplified greatly.

--

--