In my previous article Keep Trying: Redux Saga Style we focused on the ability to have a saga cleanly handle errors and continue to attempt an API call in the event of failure. Although useful, there are other extensions of this pattern that provide a bit more value; let’s take a look at a few.

Max Tries

There is one major issue with the autoRestart concept: in the case that an API is completely down you will still attempt to reconnect infinitely:

const autoRestart = (generator, handleError) => {

return function * autoRestarting (...args) {

while (true) {

try {

yield call(generator, ...args)

break

} catch (e) {

yield handleError(e)

}

}

}

}

Leveraging the above to wrap potential problem areas and prevent them from crashing your entire application is still useful, but for an API call we may want to do something more like the following:

const log = function () { ... } const multipleAttempts = (generator, handleError, maxTries) => {

return function * multipleAttempts (...args) {

let n = 0

while (n <= maxTries) {

try {

yield call(generator, ...args)

break

} catch (e) {

// Log errors here until maxTries has been hit, to prevent

// spamming the user with messages.

if (n < maxTries) {

yield log(e)

} else {

yield handleError(e)

}

}

}

}

}

In this case we simply pass along the number of times we want to attempt to connect to the API. In the event that we were unable to connect handleError is invoked, in which we setup our UI state to display the appropriate information to the user.

Time Based Attempts

I particularly like this pattern because it elegantly handles a slow connection or a user that is offline. We attempt to connect however many times we can in a given time period, then in the event that a successful connection never occurred we can treat our application as offline and handle it appropriately.

Rather than wrap an individual API call we will have this particular piece of code handle all of our API calls for this particular application.

import {

hydrateUserData, hydrateListData, setApplicationToOffline

} from 'actions' // 30 seconds max load

const maxLoadTime = 1800 // Fetch the data our application needs and store it in local state.

function * fetchData () {

const {userData, listData} = yield all({

userData: call(getUserData),

listData: call(getListData)

})

// Store or retrieved data in our Redux state.

yield [

put(hydrateUserData(userData)),

put(hydrateListData(listData))

]

return true

} // If our application is offline set some state that will disable

// UI features and notify the user.

function * setOfflineState () {

yield put(setApplicationToOffline())

} // Attempt to load our application data in a fixed amount of

// time, otherwise default to offline mode.

function * loadApplication (loadTime, fetchData, setOfflineState) {

const {offline, online} = yield race({

offline: delay(loadTime, true),

online: call(fetchData)

})

// Time won our race, thus fetchData never resolved in our

// required time and we should treat the application as offline.

if (offline) {

yield call(setOfflineState)

}

}

There is a lot going on here, so let’s break it down:

First we establish a function to handle our data needs. In this case, two API calls need to be made to get our user data and some generic list data. Once said data is retrieved we store it in our Redux state and return true. We also have a small generator to set whatever necessary state our UI requires to render in offline mode. Finally we define our logic for loading the application. We leverage redux-saga’s race effect to run our two generators. The delay helper simply returns a promise at the end of the time passed to it. The winner of the race will be available immediately after the race yields. In this instance we are not performing any additional work if online wins, but if offline wins we definitely want to put the application in offline mode.