The redux-saga module is a plugin for redux that runs generator-based functions in response to redux actions. Redux-saga generator functions are nice because they behave like co: if you yield a promise, redux-saga will unwrap the promise for you and throw a catchable error if the promise rejects. If you read The 80/20 Guide to ES2015 Generators, a simple saga should look familiar. However, redux-saga intends to keep using generators rather than async/await. In this article, I'll provide a basic example of using redux-saga, explain why redux-saga can't move to async/await, and consider whether you even need redux-saga in the first place.

Introducing Redux-Saga

Redux-saga's goal is to provide a mechanism for orchestrating complex async operations with Redux. In many ways, redux-saga is an alternative to redux-thunk, but redux-saga provides more functionality and a different syntax. For example, suppose you wanted to load some data from the GitHub API. Below is a standalone Node.js example of using redux-saga to fetch() data from the GitHub API and put it in a redux store.

const fetch = require ( 'node-fetch' ); const { createStore, applyMiddleware } = require ( 'redux' ); const { call, put, takeLatest } = require ( 'redux-saga/effects' ); const util = require ( 'util' ); const sagaMiddleware = require ( 'redux-saga' ).default(); const store = createStore(reducer, applyMiddleware(sagaMiddleware)); sagaMiddleware.run(initSagas); store.dispatch({ type: 'FETCH_USER' , name: 'vkarpov15' }); function * saga ( action ) { try { let user = yield fetch( `https://api.github.com/users/ ${action.name} ` ); user = yield user.json(); yield put({ type: 'FETCH_USER_SUCCESS' , user }); } catch (error) { yield put({ type: 'FETCH_USER_ERROR' , error }); } } function * initSagas ( ) { yield takeLatest( 'FETCH_USER' , saga); } function reducer ( state = {}, action ) { console .log( 'Action' , util.inspect(action, { colors: true , depth: 0 })); switch (action.type) { case 'FETCH_USER_SUCCESS' : return Object .assign({}, state, action); case 'FETCH_USER_ERROR' : return Object .assign({}, state, action); } return state; }

The saga() generator function looks a lot like async/await, modulo some minor differences like yield on put() . This syntax may seem strange, but it has some powerful benefits. For example, takeLatest() ensures that only the latest FETCH_USER action runs through to completion, even if you dispatch two nearly simultaneous FETCH_USER actions.

store.dispatch({ type: 'FETCH_USER' , name: 'vkarpov15' , id: 1 }); setImmediate(() => store.dispatch({ type: 'FETCH_USER' , name: 'vkarpov15' , id: 2 })); function * saga ( action ) { try { let user = yield fetch( `https://api.github.com/users/ ${action.name} ` ); user = yield user.json(); yield put({ type: 'FETCH_USER_SUCCESS' , user, id: action.id }); } catch (error) { yield put({ type: 'FETCH_USER_ERROR' , error, id: action.id }); } } function * initSagas ( ) { yield takeLatest( 'FETCH_USER' , saga); } function reducer ( state = {}, action ) { console .log( 'Action' , util.inspect(action, { colors: true , depth: 0 })); switch (action.type) { case 'FETCH_USER_SUCCESS' : return Object .assign({}, state, action); case 'FETCH_USER_ERROR' : return Object .assign({}, state, action); } return state; }

No Async/Await?

While async/await and generators are similar, the fact remains that generators are considerably more powerful for advanced users. You can transpile async/await into generators, but you can't do the reverse. As a userland library, redux-saga can handle asynchronous behavior in ways that async/await doesn't.

The takeLatest() behavior is an example of something that you can't do with async/await: you can't abort an async function once it has started unless the async function errors or returns. Because redux-saga uses generators, it is responsible for calling generator.next() to continue the function after the function yields. So cancellation is easy: just add an early return and don't call generator.next() as shown below.

const fetch = require ( 'node-fetch' ); const util = require ( 'util' ); const put = action => console .log(util.inspect(action, { colors: true , depth: 0 })); function * saga ( action ) { try { let user = yield fetch( `https://api.github.com/users/ ${action.name} ` ); user = yield user.json(); yield put({ type: 'FETCH_USER_SUCCESS' , user, id: action.id }); } catch (error) { yield put({ type: 'FETCH_USER_ERROR' , error, id: action.id }); } } function * saga ( action ) { try { let user = yield fetch( `https://api.github.com/users/ ${action.name} ` ); user = yield user.json(); yield put({ type: 'FETCH_USER_SUCCESS' , user, id: action.id }); } catch (error) { yield put({ type: 'FETCH_USER_ERROR' , error, id: action.id }); } } const cancellable = function ( generator ) { let cancelled = false ; next(); function next ( v ) { if (cancelled) { return ; } const { value, done } = generator.next(v); if (done) { return ; } if (value != null && typeof value.then === 'function' ) { return value.then( res => next(res), err => generator.throw(err) ); } next(value); } return { cancel: () => cancelled = true }; }; const call1 = cancellable(saga({ name: 'vkarpov15' , id: 1 })); setImmediate(() => { cancellable(saga({ name: 'vkarpov15' , id: 2 })); call1.cancel(); });

Do You Need Redux-Saga?

There's certainly advantages to takeLatest() , particularly if you have a good reason to want to avoid more than one action of a given type from taking place at the same time. However, is there a practical advantage to taking the latest instance of an action and cancelling previous ones rather than just taking the first one? I can't think of any use cases other than autocompletes.

If you just want to ensure only one instance of a given action runs at any one time, you can write your own redux middleware to handle it.

const fetch = require ( 'node-fetch' ); const { createStore, applyMiddleware } = require ( 'redux' ); const util = require ( 'util' ); const inflight = {}; const dedupeMiddleware = store => next => action => { if (action.payload == null || action.payload.constructor.name !== 'AsyncFunction' ) { return next(action); } if (inflight[action.type]) { return ; } inflight[action.type] = true ; action.payload(action).then( () => { inflight[action.type] = false ; }, () => { inflight[action.type] = false ; } ); next(action); }; const store = createStore(reducer, applyMiddleware(dedupeMiddleware)); store.dispatch({ type: 'FETCH_USER' , name: 'vkarpov15' , id: 1 , payload: fetchUser }); setImmediate(() => store.dispatch({ type: 'FETCH_USER' , name: 'vkarpov15' , id: 2 , payload: fetchUser })); async function fetchUser ( { name, id } ) { try { let user = await fetch( `https://api.github.com/users/ ${name} ` ); user = await user.json(); store.dispatch({ type: 'FETCH_USER_SUCCESS' , user, id }); } catch (error) { store.dispatch({ type: 'FETCH_USER_ERROR' , error, id }); } } function reducer ( state = {}, action ) { console .log( 'Action' , util.inspect(action, { colors: true , depth: 0 })); switch (action.type) { case 'FETCH_USER_SUCCESS' : return Object .assign({}, state, action); case 'FETCH_USER_ERROR' : return Object .assign({}, state, action); } return state; }

Moving On

Redux-saga is a very interesting library and it does a good job of highlighting where you might want to use generators instead of async/await. In general, I don't see much benefit to using redux-saga over plain old async/await, but the ability to cancel in-flight sagas automatically is pretty cool.

Want to learn how to identify whether your favorite npm modules work with async/await without cobbling together contradictory answers from Google and Stack Overflow? Chapter 4 of my new ebook, Mastering Async/Await, explains the basic principles for determining whether frameworks like React and Socket.IO support async/await. Get your copy!