Redux Saga: Patterns we use

Matt Fewer
Just Eat Takeaway-tech
4 min readDec 5, 2022

In this post, we will dive into Redux Saga, which is a JavaScript framework used for handling side-effects in our web application. In a previous post, I introduced us to the main concepts of Redux Saga. If you’re just getting started with Redux Saga, I recommend starting there.

The specific topics we will discuss are:

  • Running a saga once with take, instead of takeLatest or takeEvery
  • Waiting for the first finished action with race
  • Sharing services across your app with getContext

Let’s jump right in!

Running a saga once with take, instead of takeLatest or takeEvery

Inside of our slice sagas, we usually use takeLatest or takeEvery , which I covered in my previous post. For most use cases, we want to consider each action that’s detected.

But what if we are only interested in the first occurrence of an action such as the first time the user interacts with a form? What would that implementation look like in our slice saga? The Redux Saga API doesn’t offer a specific method for this, but as we are about to see, there is a straightforward way to go about it.

First, some boilerplate code:

function* changePasswordSubmittedHandler() {
// ...
}
function* changePasswordSaga() {
yield all([
takeLatest(CHANGE_PASSWORD_SUBMITTED, changePasswordSubmittedHandler)
])
}

This code will run each time the CHANGE_PASSWORD_SUBMITTED action is detected, and will cancel any previous processes started from prior calls, hence takeLatest .

Now let’s add a handler that we only want to be triggered once.

function* changePasswordSubmittedHandler() {
// ...
}
function* changePasswordFormFocusedHandler() {
yield take(CHANGE_PASSWORD_FORM_FOCUSED)
// ...
}

function* changePasswordSaga() {
yield all([
takeLatest(CHANGE_PASSWORD_SUBMITTED, changePasswordSubmittedHandler),
changePasswordFormFocusedHandler()
])
}

We’ve added changePasswordFormFocusedHandler to our slice saga. Rather than pass it as the second argument to takeLatest or takeEvery , we simply run the function inside of the array passed to all. Then, inside of the handler itself, we run take at the beginning of the function.

What’s happening here? Running changePasswordFormFocusedHandler by itself, rather than with takeLatest or takeEvery allows us to run the handler once and only once. The take statement at the beginning causes the function to pause. Only once our app detects the CHANGE_PASSWORD_FORM_FOCUSED will it unpause and run the rest of the function.

An alternative approach would be to include a flag in your app’s global state that you can flip once the first occurrence has happened. But if you want to avoid adding properties to the state, this approach works nicely.

Waiting for the first finished action with race

Sometimes we want to listen for several actions and take specific steps depending on which one of those actions finished first. For example, when a user submits payment for an order, the transaction may complete successfully or it might fail for a number of reasons. For this scenario, we turn to the race effect combinator. Redux Saga refers to race and all as effect combinators because they both accept 1 or more effects and handles them concurrently.

Let’s look at how race works:

function* paymentSubmittedHandler() {
const { failed, finished, cancelled } = yield race({
failed: take(PAYMENT_SUBMISSION_FAILED),
finished: take(PAYMENT_SUBMISSION_FINISHED),
cancelled: take(PAYMENT_SUBMISSION_CANCELLED)
})

if (finished) {
// do something
}

if (cancelled || failed) {
// do something
}
}

We create a race between 3 different effects. The first effect to resolve will be stored in the key value, so if finished is first, then we have { finished: returnedResult } . We set up conditional blocks to handle the different scenarios.

This is a useful approach to handling asynchronous requests. We use race for the following scenarios:

  • loading and initializing SDKs and libraries
  • fetching segments of user info
  • handling redirects
  • search validation
  • token management
  • UI events like the one below:
const { closed } = yield race({
confirmed: take(AGE_VERIFICATION_MODAL_CONFIRMED),
closed: take(AGE_VERIFICATION_MODAL_CLOSED)
})

In the case above, we only want to take some sort of action if the user closes the modal, hence why the result of confirmed is discarded.

Sharing services across your app with getContext

If we want to have access to certain services (or contexts) that can be shared throughout our sagas, we can utilize getContext. With some quick setup, we can easily have access to methods that can call APIs, LocalStorage, logging mechanisms, and middleware.

import createSagaMiddleware from 'redux-saga'

const cookieStorage = {
save: value => {
// save the cookie
}
}

const logger = {
logInfo: info => {
// log the info
}
}

const sagaMiddleware = createSagaMiddleware({
context: {
cookieStorage,
logger
}
})

sagaMiddleware.run(rootSaga)

When we call createSagaMiddleware, we can pass multiple contexts that we wish to access throughout our sagas. In this example, we are passing 2 contexts: one that will handle the storage of user cookie usage preference, the other is a logger to log events and messages to our internal dashboards.

After that’s done, we can access these contexts in our slice sagas with getContext.

function* cookiePreferenceStorageSaga() {
try {
const { cookieStorage } = yield getContext('cookieStorage')

const key = 'foobar'

yield call(cookieStorage.save, key)
yield put(cookiePreferenceStorageSucceeded())
} catch (error) {
const logger = yield getContext('logger')
logger.logInfo('Cookie preference storage error', { error })
}
}

From virtually anywhere in our sagas, we can import our contexts by running getContext and passing the context we want to access. Then we use the context as we need. We can use call if we are calling a non-generator function, or we can simply call the method as we do with logInfo.

Conclusion

These nifty patterns serve as reliable approaches for handling a dynamic environment like the global state of a massive web application. Perhaps in a future post, I will document some more patterns we use at Just Eat Takeaway. Let me know if you’d like to read about that!

--

--