This post assumes basic familiarity with redux-saga. Here’s the introductory tutorial.
In this post I will examine how redux-saga can be used to model some common patterns in application control-flow.
redux-saga is a library that aims to make side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) in React/Redux applications easier and better.
This is a pattern where the application is simply waiting for different kinds of actions. The first action to be received decides how the application proceeds. That is the application does not care for any actions after the first one.
For example, consider we are writing a saga for creating and sending an email. In this saga we are waiting for 2 actions
DISCARD_DRAFT
if this action is received first, the saga will discard the current draft, clean up the editor state & finish.SEND_EMAIL
if this action is received first, then the saga will probably do some validations (e.g. valid email address etc.), send the email, clean up the editor state & then finish.This flow of this saga is governed by which action (DISCARD_DRAFT
or SEND_EMAIL
) is received first. Such a situation can be modelled by simply using the [take](https://redux-saga.js.org/docs/api/#takepattern)
effect creator.
function *emailSaga() {...
const action = yield take(\[ // (A)
\``DISCARD_DRAFT`\`,
\`SEND\_EMAIL\`
\]);
if (action.type === \``DISCARD_DRAFT`\`) { // (B)
//discard the draft
} else {
//send the email
}
}
take
effect waits for any one of the 2 actions and the saga is suspended until one of them is received.type
of the received action & then proceeds accordingly.
NOTE: This situation could also be modelled using a [_race_](https://redux-saga.js.org/docs/api/#raceeffects)
effect as shown below
function *emailSaga() {const { discard, send } = yield race({ // (A)discard: take(`DISCARD_DRAFT`),send: take(`SEND_EMAIL`)})
if (discard) {
//discard the draft
} else {
//send the email
}
}
take
effects i.e. the race ends when either one of the 2 take
effects is finished.The important semantic distinction between take([...])
and race
is that
take([...])
waits for the first matching action to arrive.race
waits for the first racing-effect to finish.This again is a common pattern where we want to keep a task running until we receive a specific action to stop the task.
For example, consider we are writing a saga for adding songs to a playlist. The saga should let the user add as many songs as they like. However it should stop that task when a specific action has been received (like SAVE_PLAYLIST
).
This situation can be modelled as shown below
function *addToPlaylist() {while (true) { //(A)const action = yield take([`ADD_SONG`,`SAVE_PLAYLIST`]);
if (action.type === \`ADD\_SONG\`) {
//add the song to the playlist
} else {
break; //(B)
}
}
}
while
loop keeps the task running.SAVE_PLAYLIST
is received, we break out of the loop, there by stopping the task.
NOTE: This situation could also be modelled using a [_takeEvery_](https://redux-saga.js.org/docs/api/#takeeverypattern-saga-args)
effect as shown below
function *addToPlaylist() {const addTask = yield takeEvery(`ADD_SONG`, function*() { // (A)//add the song to the playlist});
yield take(\`SAVE\_PLAYLIST\`); // (B)
yield cancel(addTask); // (C)
}
addTask
(using takeEvery
) that receives every ADD_SONG
action & adds it to the playlist.SAVE_PLAYLIST
action.SAVE_PLAYLIST
is received the saga cancels the addTask
i.e. it stops listening for ADD_SONG
actions.This way of modelling the situation is more concise, however the previous way is more explicit about its intentions.
This is a common pattern where a business flow is broken down into smaller steps. These steps are presented to the user in an ordered way, however at any time the user is allowed to go back.
For example, consider the process of booking a flight. It can be broken down into the following 3 steps
These steps are shown to the user in the following order
Choose Flight ---> Fill Details ---> Payment
However the use should also be able to go back to the previous step.
---> --->
Choose Flight Fill Details Payment<--- <---
Such a requirement can be modelled using a parent-saga and multiple children-sagas corresponding to each step. In essence, the parent-saga controls the propagation of the process & executes the correct child-saga for the current step.
Consider we are writing a saga for the above mentioned process of booking a flight.
We assume we have the following children-sagas for the 3 steps.The contents of these children-sagas are not relevant for this discussion.
function *chooseFlight() { ... } // (A)function *fillDetails() { ... } // (B)function *paymentSaga() { ... } // (C)
BACK
action is dispatched.
function *bookFlight() { // (A)let breakLoop = false;let step = 0; // (B)
const backTask = yield takeEvery(\`BACK\`, function\*() { // (C)
if (step > 0) {
step--;
}
})
while (true) { // (D)
switch (step) { // (E)
case 0: {
yield call(selectFlight); // (F)
step++; // (G)
break;
}
case 1: {
yield call(fillDetails);
step++;
break;
}
case 2: {
yield call(paymentSaga);
step++;
break;
}
case 3: {
breakLoop = true; // (H)
yield cancel(backTask); // (I)
break;
}
}
if (breakLoop) { // (J)
break;
}
}
}
bookFlight
.step
to 0
i.e. start with the first child-saga selectFlight
.BACK
action and decrement the step
by 1
.while
loop to keep the parent-saga running continuously.switch
statement evaluates the current step
and executes the correct child-saga.selectFlight
child-saga for step 0
.step
by 1
to move to the next step.step
is 3
i.e. all the steps are completed, the parent-saga should finish.BACK
actions.
NOTE:This is a trivial example where we support only single-step propagation. However in a production app, we would want the user to jump from any step to any step (with some checks of course).
But this technique can be very easily extended to implement such a requirement (by explicitly providing the next value for _step_
variable, instead of incrementing or decrementing it).
In fact this is how we can build a finite state machine using sagas.
redux-saga is an interesting tool for modelling control-flows. I hope these patterns prove to be useful.