The one thing that was always an issue for me when it came to Redux, were Thunks. Sure, Thunks are great, and they have their place, but did you know you could replace them with something much more functional? Even though everywhere you look you see thunks, if you look around, even at Dan Abramov’s supported redux projects, you will find an amazing Redux middleware called Redux -Sagas. While I found that Sagas had strong documentation, beautifully coded, and next to no videos (not a bad thing), I figured it was time to make these a little easier to understand, and much easier scale.

First and foremost, you may be wondering, “What makes Sagas so great”? If you’re someone like myself who thrives on design patterns, especially asynchronous design patterns, you know that callbacks and promises are exactly what you find at the bottom of the barrel, and definitely want to avoid if possible. Instead of using these, lacking great functionality, Sagas prefer to use Generators, Observables, and Promises at the lowest abstract, plus control of side-effects! That should be enough reason in inself.

If at this point you are still scratching your head wondering why having these patterns available are so helpful, I would highly suggest checking out Kyle Simpson’s Rethinking Asynchronous JavaScript over at Frontend Masters. If you write a lot of async code, you won’t regret it, and if you wonder why I suggest it so often, it is by far my favorite course, and probably the only I found that was legitimately awesome with design patterns.

Minor Things (Webpack)

I plan to keep this very brief, but I will at least cover how to getting the code to transpile.

Webpack:

Start by using yarn to add babel-polyfill, babel-plugin-transform-runtime, and babel-plugin-transform-regenerator, which are required for generators.

entry: {
  js: ['babel-polyfill', './app.js']  // change entry point file as needed
 },

Then add the following .babelrc plugins: transform-runtime, transform-regenerator.

 

Seting up the store

Now we need to get the Redux store setup to start loading our sagas. Similar to reducers, we need to load the sagas one by one, but unfortunately redux-saga does not have anything built-in to allow us to load more than one at a time; like how Redux has CombineReducers for multiple reducers. That’s ok though, we can use JS ingenuity to accomplish it for us.


/App/Store/ConfigureStore.js
import ec2saga from '../sagas/s1saga'
import ec2saga from '../sagas/s2saga'
import ec2saga from '../sagas/s3saga'
// our saga array
const sagas = [ s1Saga, s2Saga, s3Saga ];

const sagaMiddleware = createSagaMiddleware();
const store = createStore(
  rootReducer,
  compose(
    applyMiddleware(
      sagaMiddleware
    ),
    DevTools.instrument()
  )
)

// normally we would load one at a time, but let's quickly 
// iterate through the array of imports instead
sagas.map((saga) => sagaMiddleware.run(saga));


Minor Things

With the sagas loaded into the store, it is finally time to put them to use. We can do exactly this by using the regular saga Watcher/Worker setup. The whole idea of making it scaleable is adding the saga loader iterator, keeping you from having to write in every single call on every saga. Here is a single saga file’s contents, to give you a rough template to go by, where it is accessed from bottom to top.

The entry point forks the process, making sure the watchers are available for any incoming action types. Once a matching action is seen, the worker generator fires, and going through its process. Again, sagas are a lot like reducers, except they are working with the actions directly, before telling the reducer to make changes to the store.


import { call, put, fork, takeEvery } from 'redux-saga/effects';
import Config from '../apis/Config';

// worker sagas
function* loadConfigAsync() {
  try {
    const res = yield call(Config.openConfig);
    yield put({type: 'LOAD_CONFIG_SUCCESS', config: res});
  } catch(error) {
    yield put({type: 'CONFIG_ERROR'});
  }
}

function* configWindowAsync() {
  try {
    yield console.log('Saga triggered!');
  } catch (e) {
    yield console.log('Error' + e);
  }
}

// watcher sagas
function* watchLoadConfig() {
  yield takeEvery('LOAD_CONFIG', loadConfigAsync);
}

function* watchConfigWindow() {
  yield takeEvery('TOGGLE_CONFIG_WINDOW', configWindowAsync);
}

// root saga / entry point
export default function* configSaga() {
  yield [
    fork(watchLoadConfig),
    fork(watchConfigWindow)
  ]
}

 

Scaling Multiple Sagas

So the above is a great way to scale your sagas, and another way thanks to Matt Granmoe from the redux-saga Gitter channel also pointed out using the regular method mixed with the Duck File Structure is also a great way to accomplish this, and I believe the file structure the saga developer is catering to.

 

Taking it farther

Of course sagas have much more capability, and suggest checking out the documentation and Gitter chat. Sagas are incredibly amazing, and even if you do not understand everything yet, once you do, you will understand the genius at play.

Overall, redux-sagas are the best way to handle asynchronous calls, as well as a must have middleware; assuming you are comfortable with ES6 generators. If you are still lost with generators, or feel you have more than enough with thunks, I still want to remind you again about Rethinking Asynchronous JavaScript. Not only will you learn about generators, but also understand exactly why various methods are better than others.

Enjoy!