Getting Familiar with Redux Saga

Eleni Chappen
20spokes Whiteboard
4 min readMar 11, 2017

If you’ve tried other Middlewares in Redux, like Redux Thunk, and have found them difficult to read and maintain, it may be worth trying redux-saga.

Lots of our React/Redux projects at 20spokes require many different things to happen when a user performs a single action — new information needs to be fetched, audits need to be updated, input needs to be saved, etc. Using sagas to perform these actions has made our code much more understandable.

Redux-saga is known to have quite a learning curve. Before you dive into sagas, it’s best to understand how generator functions work, and how sagas use them.

Generators

Without getting too in-depth, generator functions are essentially functions that can be paused and resumed. Generator functions are paused using the yield keyword, and resumed by calling next() on the function. Redux-saga takes care of pausing and resuming generator functions for you. Your job is to provide the proper return values for each yield block using redux-saga’s own helper functions, most commonly put, call, and fork. (These functions are called “effect creators” in the documentation.)

Generator functions are specified using function*. Here’s an example of one of our redux-saga generator functions:

function* beginSession() {
yield fork(fetchRate)
yield call(createAudit)
yield put({ type: 'SET_LAYOUT', payload: 'application' })
}

The greatest convenience of redux-saga is the ability to run concurrent tasks, and to write all of this activity in a streamlined way.

For instance, there are actually two tasks running concurrently in the saga above. The first task is fetchRate, which is running independently usingfork . The second task is first calling createAudit using call, then dispatching the SET_LAYOUT action once createAudit is complete using put. One of these tasks can finish before the other, and that’s okay, because they’re not reliant on each other.

Using Generators with API calls

Here’s an example of a saga that handles a return value from an API call:

export function* fetchRate(){
try {
yield put({type: 'FETCH_RATE_REQUESTED'}) const response = yield call(getRate)

yield put({
type: 'FETCH_RATE_SUCCEEDED',
payload: response.current_rate
})
} catch (e) { yield put({
type: 'REQUEST_FAILED',
payload: { message: e.message }
})
}
}

A few things to note here: First, we’re wrapping the API call in a try/catch block. Second, we’re using call to make the api call, which means that the next yield block that uses the response data will not be executed until the api call has been resolved. Lastly, we’re dispatching Redux actions that announce the start of the request, its success, and its potential failure. This is super helpful for debugging purposes.

We typically use fetch to perform our API calls. We created separate api tools outside of our sagas that look like this:

function api(ourFetch) {  return ourFetch.then(function (resp) {      return resp.json()    }).then(function (json) {      // any custom error handling here
if (json.session_error) throw new Error(json.session_error)
return json
})
}
export const getRate = function() {
return api(fetch('http://myendpoint.com/rate', {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
}))
}

This is the part that tripped me up about sagas when I was first learning them: the return value of whatever you give to Saga’s call should be a resolved Promise. Resolving your fetches outside of your sagas makes them a lot more readable, modular, and testable.

Testing Sagas

I’ve found testing sagas to be very simple and synchronous using a package called redux-saga-testing. Redux-saga-testing takes over your tests and passes the resulting generator object of each yield block for every test. (Notice below how each it corresponds to each yield in fetchRate above.)

I like this package because all you have to do to mock your return value to yield (like your return from an API call), is to return a mock value for it:

// Tests Using Mocha and Chaiimport sagaHelper from 'redux-saga-testing'describe('fetchRate() on success', () => {
const it = sagaHelper(fetchRate())
it('notifies of api request', result => {
expect(result).to.eql(put({type: 'FETCH_RATE_REQUESTED'}))
})
it('calls api', result => {
expect(result).to.eql(call(getRate))
// mock response from api
return { current_rate: 100 }
})
it('updates store with current rate', result => {
expect(result).to.eql(put({
type: 'FETCH_RATE_SUCCEEDED',
payload: 100
}))
})
})

No need for any stubs or mock-fetching tools!

Accessing state data in sagas

Sometimes in a saga you’ll need to access state data from the Redux store. Redux-saga has a method for this called select. Here’s an example of a saga that uses select:

const getAudit = (state) => state.audit
const getCurrentStep = (state) => state.layout.currentStep
export function* updateAudit(action) {
try {
yield put({ type: 'UPDATE_AUDIT_REQUESTED'})
const audit = yield select(getAudit)
const currentStep = yield select(getCurrentStep)
let data = {
...audit,
...action.payload,
last_step: currentStep
}
const response = yield call(Api.updateAuditPost, data) yield put({
type: 'UPDATE_AUDIT_SUCCEEDED',
payload: response.audit
})
} catch (e) {

yield put({
type: Requests.REQUEST_FAILED,
payload: { message: e.message }
})
}
}

Note that you have to call select within a yield block just like you do with call, put, and fork.

Suggestions for use

Redux-saga is a middleware, which means that sagas are interrupting certain Redux actions that you specify (see the docs for takeEvery and takeLatest). If you’re using separate files to define your reducers, actions, and sagas, it can be difficult to see what actions are being caught by the middleware. This is why it’s good to have some kind of naming convention for all action types that are caught by sagas. I started appending _SAGA to these action types (for instance, FETCH_RATE_SAGA). The documentation uses _REQUESTED, but I found this convention too conflicting with other actions that I was dispatching for network requests. Tomato, tomahto.

Conclusion

I’ve only scratched the surface here on redux-saga, but you can accomplish quite a bit with the methods described.

Overall I have a very positive impression of sagas: they’re easier to reason about and much, much easier to test. They’ve also taken a lot of action dispatching out of our React components, resulting in a more predictable, MVC-like pattern in our React projects.

--

--