Anatomy of a React application: saga

Anatomy of a React application: saga

As your React application grows in terms of functionality, handling the state and its updates in a clean way becomes more and more important. This chapter will cover a library that allows you to have better control of the side effects of your React application.

Before we start looking at redux-saga itself, let’s find out a little bit more about sagas.

"A saga is long lived transaction that can be written as a sequence of transactions that can be interleaved”. “All transactions in the sequence complete successfully or compensating transactions are run to amend partial execution” (Hector Garcia-Molina, Kenneth Salem - Princeton University 1987).

In traditional models, a transaction is atomic and either gets committed or rolled-back to a single source of truth database or datastore. This solution is difficult to scale and not well suited to microservices architectures where data is often spread across different systems and data stores.

With sagas, single interleaved (meaning not dependent on each other) atomic sub-transactions can be grouped into sequences called sagas. Such transactions can then have compensating transactions (also independent, atomic, and interleaved) which are used to revert to a safe system state in case of failure. The main conceptual differences between sagas and traditional atomic-based systems is that pure rollback might not be possible using a compensating transaction and alternative actions might need to be triggered (e.g. if an email was sent out, it will not possible to undo such action but it might possible to send a follow-up email). This is referred to as failure management system and allows you to semantically undo an action and preserve a safe system state as opposed to rollback a transaction.

This method trades off atomicity for availability (saga transactions are not atomic) whilst still maintaining consistency across the system. It also puts failure management at the core of the design. This is particularly important in distributed / microservices architectures as any of the components could potentially fail and dealing with errors / recovery / fallbacks can become difficult to achieve. Being forced to consider failure management from the ground up, can also help you creating a more robust architecture. The downsides of this approach include: compensating requests could themselves fail and would need to be dealt with (possibly retrying until they succeed); a saga might crash leaving the system in an unknown state (possibly making it difficult to understand what failed) and transactions could not be replayed safely (in this case compensating transactions should be run to bring the system back to a safe state).

Now that we have a better understanding of sagas in general, let’s dive into redux-saga: a library loosely based on the saga pattern and aimed at managing async flows.
If you are coming from a redux-thunk implementation (see template from previous parts of this series), redux-saga won’t be that dissimilar. Components will still dispatch actions as plain objects.
Sagas are implemented as generator functions yielding objects (Effects) to the redux-saga middleware. Effects are simple JavaScript objects containing instructions to be fulfilled by the middleware and can be combined together in sequences to form flows.
During the execution of a flow, redux-saga pauses until the result of an Effect is yielded, this then gets picked up by the middleware which dispatches the correct action.

Here are some examples of redux-saga Effects:

  • takeEvery is the most common and similar to redux-thunk; it allows multiple requests to start concurrently;
  • takeLatest allows only one request, takes the latest, and cancel previous tasks if present;
  • cancel can be used to cancel a task;
  • all runs tasks in parallel (like Promise.all);
  • race starts tasks in parallel but wait only for one to complete, cancelling automatically losers tasks.
More saga Effects are available from the redux-saga/effects package, and their APIs can be found here.

The declarative nature of sagas offers better control of flows. Steps from the same flow can be kept together and actions can be pulled rather than pushed using the take* Effects.
This approach, combined with the use of generators, also means that the code can be easily tested by iterating over the generator functions and using equality assertions.

Given the following code:

import { call } from 'redux-saga/effects';
import Api from '...';

function* fetchData() {
  const data = yield call(Api.fetch, '/get-data);
  // ...
}
Testing can be achieved by doing:
const iterator = fetchData();
assert.deepEqual(iterator.next().value, call(Api.fetch, '/get-data’);

Let’s now have a look at the steps required to implement sagas on the web application template:

  • Replace redux-thunk with redux-saga: yarn remove redux-thunk && yarn add redux-saga --save
  • Add babel support for generators by installing and configuring the required babel libraries (see package.json and .babelrc);
  • Replace actions and redux middleware with sagas. The timer sagas show how actions can be pulled rather than pulled as previously implemented in the redux middleware;
  • In configure-store.js replace the redux-thunk middleware with redux-saga and start your rootSaga.
The complete redux-saga template can be found here.

Conclusions
Using sagas on your React application can have great advantages. Various transactions and interaction with APIs can be grouped together to make the flow of your application easier to understand and manage. Testing also becomes easier when using generators and saga Effects. On the other hand, generators might be slightly confusing and harder to understand for those who haven’t used them before.
In this chapter’s template and examples, sagas are only used to explain a different way to dispatch actions but as your application becomes more complex, the benefits of using sagas should be noticeable and make developers’ life easier.