Telling a (redux) Saga

Written by meeshkan | Published 2017/11/01
Tech Story Tags: redux | redux-saga | kitten-fetch | kittens | coding

TLDRvia the TL;DR App

At Meeshkan, we are heavy users of [redux](http://redux.js.org/) in our web and mobile apps. After first using [redux-promise](https://www.npmjs.com/package/redux-promise) and [redux-thunk](https://github.com/gaearon/redux-thunk) middleware, we are now happily using [redux-saga](https://github.com/redux-saga/redux-saga) as our way to deal with asynchronous operations. If you are not using [redux-saga](https://github.com/redux-saga/redux-saga) yet in your React apps, you should drop what you’re doing and read the tutorial!

The Saga Pattern is a great way to describe side effects through pure functions. There are many different ways to understand the pattern, but in my own mind, I like to think of it as a script. Take, for example, the saga:

import { call, put } from 'redux-saga/effects';import fetchKittenPictures from './kittens';

function* fetchKittenPicturesSaga(action) {try {yield put({ type: 'FETCH_KITTEN_EXECUTING' });const critters = yield call(fetchKittenPictures);yield put({ type: 'FETCH_KITTEN_SUCCESS', payload: critters });} catch(e) {yield put({ type: 'FETCH_KItTEN_FAILURE' });}}

Giving this script to an actor that will declaim my saga, she will triumphantly pronounce:

KITTEN FETCH (a play in one act)

Teller: I am executing a kitten fetch!Teller: I am making a call to my kitten fetch api![she goes offstage, comes back]Teller: I have my kittens.[in the event that she breaks her leg, she yells "NO KITTENS!"]

But what if we gave our saga to the late great Robin Williams. Would he follow our script? No! It may look something like this:

KITTEN FETCH (a play in one act)

Robin: I am executing a kitten fetch!Robin: Wait, kittens don't fetch, dogs fetch. And why are we executing kittens? That's cruel.[pauses for laughter]Robin: Ah, we're talking about an API. I could sure use an IPA![pauses for laughter]Robin: I am making a call to my kitten fetch api![he goes offstage, comes back]Robin: I have my kittens.[in the event that he breaks his leg, he yells "NO KITTENS!"]

A different teller will tell the saga differently.

In the world of React development, where sagas contain core business logic, sometimes sagas need to change slightly depending on the platform on which they’re running. For example, we may need two “tellers” for the same saga — one for web and one for mobile. They need to be able to share most of their “script” while making slight adjustments based on their unique way of telling the saga. An anti-pattern would be to hardcode into a saga all of the different ways it could be told. The cleaner solution is to give the saga to a teller and have her tell it.

So, we have put together a hack called saga-teller that does exactly this. The API is dead simple:

function sagaTeller(saga, rules);

The first argument, saga, is the saga that the teller will tell, and rules is a blueprint for how the teller will modify the saga.

The rules object itself is in the form:

// match// note that put and call should be mutually exclusive

{when: <'before' or 'after'>,put: <an action if we are responding to PUT>,call: <a function if we are responding to CALL>,effect: <any valid redux-saga effect like all, put, take...>}

// rules{first: <call or put>,matches: <Array of match>,last: <call or put>}

Using our kitten saga, we may do this:

const myVeryOwnFetchKittenSaga(fetchKittenSaga, {first: call(lookMomIKnowHowToUseSagas),matches: [{when: 'after',put: 'FETCH_KITTEN_EXECUTING'effect: put({type:'MOM_YOU_SHOULD_BE_MORE_PROUD_OF_ME'}),},],});

And there you have it. Now, when I tell the saga, the first react-saga will do is call a function telling my mom that I know how to use sagas. Then, the fetchKittenSaga chugs along, and after it emits a put action with FETCH_KITTEN_EXECUTING, it sends out a MOM_YOU_SHOULD_BE_PROUD_OF_ME action. This way, the original saga stays intact and my personal saga can be used wherever I want. This is also really easy to test—you just need to assert that the rules object contains the correct logic.

In a more real-world example, this allows you to write one saga and then write a few rules that tweak its behavior based on if you are web or mobile, Android or iOS, etc. Note that the saga-teller pattern is a goal line strategy. You should never drive your app down the field with it. Think of it as a sort of patch for your saga — it should represent the minimum amount of work needed to modify a core saga, but anytime the teller’s version dwarfs the saga itself, it becomes an unwieldy kludge. In this case, just write (and test) two different sagas.

Below is a gist that shows a rudimentary implementation of a saga-teller. The tell function at the bottom just allows you to chain together multiple tellers, so you can do tell(mySaga).by(teller0, teller1, teller2). Think of this like the telephone game, where each teller hears the version of the previous one and modifies it with their special addition.

I will definitely not be making anything other than a gist for this because it is based on a redux-saga internal API and is really fragile in case of updates. But it is short enough that you can copy it, paste it, and modify it to your heart’s content. We’ve been using a version of it at Meeshkan with lots of success. Enjoy telling your sagas!


Published by HackerNoon on 2017/11/01