Don’t Call Me, I’ll Call You: Side Effects Management With Redux-Saga (Part 2)

David Dvora
AppsFlyer Engineering
8 min readOct 29, 2018

In the first part of the blog, I presented the basics of Redux-Saga, and the integration with React. Now I’m going to provide some examples which mimic real life data flow challenges and how Redux-Saga is addressing them while keeping the code efficient and clean.

Request Dependency Management

Let’s say we want to fetch a chunk of data from a server, then fetch another chunk which depends on the first one, etc. This flow is similar to promise chaining.

The following illustration represents a page with the above mentioned scenario, where each change in drop-down A, sends a request for data which will be used for populating B’s options. Once we get the options for B, we use the first option to fetch the options for C.

Figure 1

We can achieve the desired behavior with Saga, by yielding ‘call’ with a GET request to server (GET is a function which sends Ajax and returns a promise):

// saga.js
import { call, put, take } from 'redux-saga/effects'
function* mySaga(){
// user's selection dispatches DROP_DOWN_A_CHANGE_ACTION
const { dropdownAValue } = yield take(DROP_DOWN_A_CHANGE_ACTION);

// fetch the options for B according to selection
const bOptions = yield call(GET, `FETCH_B_OPTIONS_URL/${dropdownAValue}`);
// populate B's options data on state
yield put('UPDATE_DROP_DOWN_B', bOptions);

// fetch the options for C according to B's first option
const cOptions = yield call(GET, `FETCH_C_OPTIONS_URL/${bOptions[0]}`);
// populate C's options data on state
yield put('UPDATE_DROP_DOWN_C', cOptions);
}

There is only one active request at each point in time, since after every ‘yield call’ the Saga is blocked until we get a response from server. Using Sagas here makes this code seem synchronic as opposed to promise chaining.

Eliminate Race Conditions

Let’s discuss this simple flow:
1. The User selects a value in a drop-down menu
2. The Client fetches a chunk of data from the server, according to that input

If the User is able to change the drop-down value while the request is still in progress, we can get a “race condition,” meaning the response from the first request can arrive after the response from the second one.

This is an undesired behavior, forcing us to implement a logic which will ignore the old responses, while using only the latest. If we use promises, we can achieve this by storing the latest drop down value and check whether to ignore a response upon every completion.

takeLatest is a powerful Saga effect, which cancels previous Saga executions per action, once a new action is dispatched:

// saga.js
import { call, put, takeLatest } from 'redux-saga/effects'
function* handleInput(action){
const item = yield call(GET, `https://my.server.com/item/${action.dropDownValue}`);
yield put(UPDATE_ITEM_ON_STATE, {item});
}
function* mySaga(){
yield takeLatest(DROP_DOWN_CHANGE_ACTION, handleInput);
}

Let’s review the basic flow:
- The User selects a drop-down value, which dispatches ‘DROP_DOWN_CHANGE_ACTION’
- handleInput is executed and we fetch the relevant data from server
- The Server returns a response
- We dispatch ‘UPDATE_ITEM_ON_STATE’, with the response from server

So what will happen if the user changes the drop-down value before the first response comes back?
- User selects a new drop down value, which dispatches ‘DROP_DOWN_CHANGE_ACTION’ again with new value
- handleInput’s first execution stops yielding (since we used ‘takeLatest’)
- The Server returns a response for the new value
- We dispatch ‘UPDATE_ITEM_ON_STATE’, with the new response from server
- The Server returns a response for the first value, but since we stopped yielding, ‘UPDATE_ITEM_ON_STATE’ won’t be dispatched!

In this case, we don’t need to worry about handling race conditions because only the latest output from server will be used.

It’s important to note that mySaga and handleInput have different roles in the application. mySaga is a Saga which runs on the bootstrap phase (you can think about it as a ‘redux actions listener’), while handleInput runs on demand, like a regular generator function.

We usually execute only the Sagas which function as ‘redux actions listeners’ on application start.

Eliminate Redundant Requests To The Server

By combining the first two examples, we can see how powerful Redux-Saga is. Imagine that you have a page with some kind of input. Each input change updates the page with data from the server by executing two or more requests which are dependent on one another.

Using ‘takeLatest’ ensure we don’t continue to execute any more requests after input changes by making the active Saga stop yielding when a new action is dispatched.

Once we apply ‘takeLatest’ on the code behind Figure 1, we introduce a new behaviour:
1. The User change the value in A and fetching the data for B is starts.
2. The User change A’s value again, while the data for B is still being fetched.
3. The ‘yield call’ for C’s data is skipped since this Saga stopped yielding after the latest change of the value in A.
4. C will be populated with the correct data given from the latest Saga execution.

This is one of the most powerful Saga features, because it provides clear data flow control and also makes network resource consumption more efficient.

Perceived Performance

Sometimes we want our web page to fetch the data in a specific order, in order to make the page load faster or at least seem to load faster. For example, we can do that by giving priority to the requests which populates the top part of the page first. That way those requests won’t compete on the browser’s network resource (max concurrent requests per domain), with requests which are used for fetching data for the part of the web page which is not necessarily visible yet.

In order to demonstrate this, please focus on the following illustration:

Figure 2

Imagine this is the main application’s page, where all elements but D are visible above the fold. Let’s say every element requires a separate server request in order to be populated with data.

We don’t want D to start fetching its data right on page load and compete with A/B/C. We would want those actions to be completed first. For the sake of the example, we would also like to give priority to A and B over C.

Let’s configure actions which will be used for this example, for changing the store and then populating the data on the page. ‘UPDATE_CONTENT_X’ will be the action for updating widget X.
FETCH_X_URL’ constant is the URL used for fetching the data for widget X.

// saga.js
import { call, put, all, take } from 'redux-saga/effects'
function* mySaga(){
const { dropdownValue } = yield take(DROP_DOWN_CHANGE_ACTION);

const [aData, bData] = yield all([
call('GET', `FETCH_A_URL/${dropdownValue}`),
call('GET', `FETCH_B_URL/${dropdownValue}`)
]);
yield put('UPDATE_CONTENT_A', aData);
yield put('UPDATE_CONTENT_B', bData);

const cData = yield call(GET, `FETCH_C_URL/${dropdownValue}`);
yield put('UPDATE_CONTENT_C', cData);

const dData = yield call(GET, `FETCH_D_URL/${dropdownValue}`);
yield put('UPDATE_CONTENT_D', dData);
}

First, we wait for the ‘DROP_DOWN_CHANGE_ACTION’ to be dispatched with the value we need (‘dropdownValue’). Then we execute requests for both A and B concurrently, using the all effect, which ‘blocks’ execution until both ‘call’s are resolved (when both of the responses are back from server).
Process continues with fetching data for C, and only after we get it, we continue to D. After every ‘call’ is resolved, we update the UI using the relevant action and data.

This kind of code allows us to determine which (and how many) requests could run concurrently and also choose the execution order. That way users will see the top part of the page populated first, since content is loaded top to bottom. On a complex page which sends many requests, the need for this technique becomes more apparent.

Unleashing Concurrency

So far I explained how to limit and pace the Saga executions, but on the other hand Sagas allow us to trigger flows which are not dependant on each other, in a non-blocking manner.

Using fork and spawn, we can execute a new ‘Saga thread’, which is actually executing a new Saga from an existing Saga’s context. This is convenient when we have a complex task, built from smaller tasks which can run independently from one another.

An example of this is simply a couple of server requests where each one affects another portion of the web page. We would like those requests to be fetched at the same time which is actually the default behavior of a standard web page.

Let’s take a scenario where we fetch some data from the server and use it for completing some tasks. Those tasks are not dependent on one another, but we still want to control the execution flow inside each one.

// saga.js
import { call, put, fork, take } from 'redux-saga/effects'
function* task1(data) {
/* do some processing, server calls etc. */
yield put('UPDATE_STATE_WITH_TASK1_RESULTS', results);
}
function* task2(data) {
/* do some processing, server calls etc. */
yield put('UPDATE_STATE_WITH_TASK2_RESULTS', results);
}
function* mySaga(){
yield take(STARTING_ACTION);

const data = yield call(GET, `FETCH_DATA_URL`);
yield fork(task1, data);
yield fork(task2, data);
const otherData = yield call(GET, `FETCH_OTHER_DATA_URL`);
...
}

Both ‘task1’ and ‘task2’ run on their on, and do not block mySaga’s execution. This means the lines afterwards the forks will continue the execution right after forking task1 and task2. At that point, you allow Saga/JavaScript to do its best to take over the execution order. Let’s say both tasks just send a request to the server, those requests will most likely be fetched at the same time, while both tasks won’t be block each other.

Since every ‘fork’ returns a task instance, we can also utilize the task cancellation feature to cancel a task on demand. For example, if you have multiple, time-consuming tasks, you can allow the user to send an ‘abort’ event, which will cause the tasks to stop yielding.

The difference between fork and spawn is the relation to the Saga they are executed from. While fork errors bubble to the executing Saga and fork execution is cancelled when the executing Saga is cancelled, spawn acts like a detached process, and runs on its own - like a root level Saga. Read more here

Conclusion

There are many more interesting use cases which Sagas addresses, like simulating a timeout using ‘race’ effect, throttling and debouncing input, waiting for forked tasks to complete using ‘join’ and more.

I find Redux-Saga a very convenient tool for managing React/Redux based pages life-cycle. When building complex web applications, Sagas makes the side-effect parts more understandable and controllable. It also keeps the reducers clean from all side-effect, which makes the testing easier.

Love forking Sagas into the wild? Join us!

--

--