We code the web

Handling async in Redux with Sagas

Using Redux is a nice way to code structured, testable Javascript applications. But there’s still one thing that can prove to be a challenge, asynchronous operations.

Note: This article was originally written January 23, 2016, because redux-saga changed a lot since, it is updated for the latest version as of October 1, 2016. Most notably while (true) { take(ACTION) } is replaced with takeEvery (old version still works though) and you now need to have one ‘root saga’.

If you don’t know about redux, you can check out my article about it:
“Functionally managing state with Redux”

Asynchrony is not hard to achieve in Javascript, the language is perfectly suited for it. But testing async behaviour and implementing it in a structured way, is another story.

Actions

In redux, the events happening in the application are called Actions. Actions are just plain objects containing a description of an event, they get dispatched to the store which handles them further. Actions are made by convenience functions called Action creators. Because of this, the one dispatching the action, does not need to know it’s exact structure.

1
2
3
4
5
6
7
8
9
function saveScore (score) {
return {
type: SAVE_SCORE,
score
}
}
// usage
store.dispatch(saveScore(9001))

This means you can also do some logic before returning the Action itself. However, this logic needs to be synchronous, because in the end you need to return a plain object. But what if you need to do async logic?

Async actions

Say we want to save the score to the server. We want to dispatch actions as follows:

1
2
3
=> SAVE_SCORE
(save to server)
=> SAVE_SCORE_SUCCEEDED

We could use redux-thunk middelware for this. This middleware allows us to dispatch functions instead of plain objects. These functions will be passed a reference to store.dispatch. Here’s how we could implement our save score action with redux-thunk middleware:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function saveScore (score) {
return dispatch => {
dispatch({
type: SAVE_SCORE,
score
})
fetch('/scores', { method: 'POST', { score } })
.then(dispatch => {
dispatch({
type: SAVE_SCORE_SUCCEEDED
})
})
}
}

This looks pretty good, there’s a nice overview of what happens. The downside is testability. A lot of async action is happening inside the Action creator:

  1. A function is returned that first dispatches an action.
  2. Fetch is called which returns a promise.
  3. A callback is registered at the promise, which on it’s turn dispatches an action.

A test would look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
it('Save score posts the score to the server and dispatches a success action.', done => {
const server = sinon.fakeServer.create()
server.respondWith('POST', '/scores', [
200,
{ Content-Type: 'application/json' },
'{ "success": true }'
])
const mockStore = createMockStore([ thunk ])
const expectedActions = [
{ type: SAVE_SCORE, score: 9001 },
{ type: SAVE_SCORE_SUCCEEDED }
]
const store = mockStore({}, expectedActions, done)
store.dispatch(saveScore(9001))
server.respond()
server.restore()
})

This is fine and it works. But imagine having a lot of actions doing API calls and also having to test error handling. It will be more work doing and debugging all the mocking and setup, instead of actual testing.

Using middleware

Why not use middleware, we could write generic middleware that does requests, and use it in our action like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function saveScore (score) {
return {
type: SAVE_SCORE,
score,
request: {
method: 'GET',
url: '/test',
success (dispatch) {
dispatch({
type: SAVE_SCORE_SUCCEEDED
})
}
}
}
}
// usage
store.dispatch(saveScore(9001))

But then your Action creators still need to pass a callback along with the Action. So although they are now vanilla actions again, to test their behaviour you still need to do all the mocking.

Middleware runs every time an action enters the store, before it reaches the reducer. Middleware is mostly used to perform generic operations on certain or all actions, like logging or doing requests.

A Saga

Redux-saga has a really nice solution for this issue. It separates the async logic from the action by putting it into ‘Sagas‘. Sagas can be seen as little stories that describe the behaviour of one or more Actions.

Sagas work like daemons that run in the background. Redux-saga’s ‘sagaMiddleware‘ runs and controls these Sagas. The Sagas themselves are ES6 generator functions, this allows redux-saga to have control over every single step the Saga does.

This is how the Saga for our action would look:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function *saveScoreSaga() {
// Wait for (every) SAVE_SCORE action
takeEvery(SAVE_SCORE, saveScore);
}
function *saveScore({ score }) {
try {
// Tell redux-saga to call fetch with the specified options
yield call(fetch, '/scores', { method: 'GET', body: { score } })
// Tell redux-saga to dispatch the saveScoreSucceeded action
yield put(saveScoreSucceeded())
}
catch (err) {
// You get it
yield put (saveScoreFailed(err))
}
}
const sagaMiddleware = createSagaMiddleware()
const createStoreWithSagas = createStore(reducer, applyMiddleware(sagaMiddleware))
sagaMiddleware.run(saveScore)

Let’s break this down, first of all function * means that this is a ‘generator function’. By yielding takeEvery we tell redux-saga to call the saveScore generator function.

Generator functions are an ES6 feature. For more information on generators, David Walsch has an awesome article on this subject.

Right of the yield keyword, you can place a value you want to return to the caller. On the left, you can retrieve a value back from the caller. Both are optional.

Now we fetch the scores, notice that we do not call fetch directly. We use redux-sagas call function, which creates an object that tells the sagaMiddleware to call fetch with the given arguments. We yield this object to the sagaMiddleware. The sagaMiddleware will now execute this call and when it’s done, resume the Saga. This means that to us, this just feels like a synchronous function!

The function you pass to call needs to return a Promise. This way redux-saga can recognize it as an asynchronous operation and wait for it. There also is a cps function for functions that take a callback instead of returning a Promise.

If the call succeeds, we yield a put object with an action of type SAVE_SCORE_SUCCEEDED. This tells the sagaMiddleware that we want to dispatch an action. When the request fails we put an action of type SAVE_SCORE_FAILED.

Our action creators can now just return plain and simple objects:

1
2
3
4
5
6
7
8
9
10
11
12
13
const saveScore = score => ({
type: SAVE_SCORE,
score
})
const saveScoreSucceeded = () => ({
type: SAVE_SCORE_SUCCEEDED
})
const saveScoreFailed = err => ({
type: SAVE_SCORE_FAILED,
err
})

These action creators will be a breeze to test! This makes you wonder if we even need them anymore. I think there still are some use case where you want to do some simple, synchronous operations in an action creator.

To test the saveScore function you would need to export it. We can test it like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
test('Puts a success action when the request succeeds.', t => {
const it = saveScore({ score: 7 })
t.deepEqual(
it.next().value,
call(fetch, '/scores', { method: 'GET', body: { score: 7 } })
)
t.deepEqual(
it.next().value,
put(saveScoreSucceeded())
)
})

Look at how simple that test is! By calling it.next(value) we can continue to the next step, optionally passing a value. This is exactly what the sagaMiddleware also does when running the Saga. The result of each it.next() has a value property, containing the value the generator yields at that point. Because we yield plain objects, we can just use a deep equal to check if they are correct. To test the saga that calls takeEvery we need to use call to call takeEvery:

1
2
3
function *saveScoreSaga() {
call(takeEvery, SAVE_SCORE, saveScore);
}
1
2
3
4
5
6
7
8
test('Takes every save score action.', t => {
const it = saveScoreSaga()
t.deepEqual(
it.next().value,
call(takeEvery, SAVE_SCORE, saveScore)
)
})

You can argue that the last test is not that useful, but at least now you know what to do if code coverage is important to you or your team.

Nesting Sagas

It is also possible to nest Sagas. Let’s say we have a generic requestSaga for doing requests, maybe because we want to do some default error handling in there:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function *requestSaga (method, url, body) {
return { err: error, res: result }
}
function *saveScoreSaga() {
takeEvery(SAVE_SCORE, saveScore);
}
function* saveScore({ score }) {
const { err, res } = yield call(requestSaga, 'POST', '/scores', { score })
if (err) {
yield put(saveScoreFailed(err))
} else {
yield put(saveScoreSucceeded())
}
}

Instead of calling an asynchronous function, we can call another Saga. This will wait for the other Saga to finish and then continue.

Sequential Sagas

By using takeLatest we can make a saga only take the latest action, when a saga is already triggered and a new action comes along the old one is cancelled. In some cases this is desired behaviour:

1
2
3
function *saveScoreSaga() {
takeEvery(SAVE_SCORE, saveScore);
}

Multiple Sagas

It is not possible (anymore) to register multiple saga’s at the saga middleware. Just like with reducers in redux, you need a root saga that calls to other saga’s.

1
2
3
4
5
6
7
8
9
10
11
function *rootSaga() {
yield [
saveScoreSaga(),
otherSaga()
]
}
const sagaMiddleware = createSagaMiddleware()
const createStoreWithSagas = createStore(reducer, applyMiddleware(sagaMiddleware))
sagaMiddleware.run(rootSaga)

The rootSaga yields an array of saga’s. These saga’s should already be called, so you actually yield an array of iterator objects.

You only need to yield the Sagas that actually take actions or that you want to execute immediately. The others can be considered ‘sub’ Sagas, only called by others.

Why not use ES2016/Next async await?

Async functions are a new feature coming to the next version of EcmaScript. They are currently in stage-3, meaning they are a candidate for the next version. They work as follows:

1
2
3
4
5
6
7
8
async function saveScore (score) {
try {
const res = await fetch('/scores', { method: 'POST', { score } })
// Do something
} catch (err) {
// Handle error
}
}

This function is marked as asynchronous by the async keyword. As soon as the the async function hits the await keyword, it waits for the promise behind it to resolve, then continues. This really nice and clean. But then again, it does not solve the issue of testability. This async function actually calls fetch, while the Saga merely yields a description of what to call. The latter makes unit testing dead easy, saving you from having to mock anything.

Conclusion

Sagas are awesome, they are a really nice way of doing async in redux. They succeed at separating concerns and making testing really easy. It might be confusing at first, but once you start trying Sagas out it all starts to make sense. There are other implementations out there for handling side effects, like redux-side-effect, redux-effects and redux-loop. But, in my opinion, none of them solves it as elegantly as redux-saga. Happy coding!

Reference

Related posts

Flux, what and why?

If you are into front-end development, you’ve probably heard or read the term ‘Flux’. What does it mean and why should you care?

Functionally managing state with Redux

The Flux application design pattern is still going strong and gaining popularity. There are countless libraries around, helping you implement Flux. But lately, one has been standing out.

Immutable Javascript using ES6 and beyond

Writing immutable Javascript code is a good practice. There are some awesome libraries around like Immutable.js to help you with this. But could we survive with just vanilla, next generation Javascript?