Redux-saga common patterns

A collection of useful patterns that make our life easier on daily basis.

Photo by Lauren Mancke on Unsplash

This is a 2 part serie, feel free to check the first part here:

I’ve been a redux-saga user since a year now and I still remember when I was introduced to the library and how amazed I was with that ‘Eureka’ moment when I solved a few problems in matter of hours. It was so damn good that I needed to share all this awesomeness with people so I sat down and wrote a post about it, from that day, due to the curse of knowledge, I cannot imagine my life without it.

It was so good that also I use nowadays as orchestration layer to manage all the asynchronous operations at work at Shiftgig, so yes, this lines of code help to support daily operations of a several million VC companies up to a point that a big part of our application architecture relies on it. if you are more interested about this other post can point you on the direction.

Aside note: If your company happens to rely heavily on this or any open source project, I highly encourage you (and your employer) to become a backer of the project, this small donation can make a big difference and it’s a nice way to keep karma in place 🙏🏾.

Enough for the emotive introduction, let’s see the patterns.

This post will assume that you have a basic understanding of the library, if you need more information about how to register a saga or even how to configure please refer to the first part of the serie here.

After a year of working with the library solving problems, we have identified a few patterns that we repeated over an over, let see them one by one with a possible use case for each.

Disclaimer: I put the names of these for some reason I don’t even know, if you happen to know them with some sort of official name, please let me know.

Take and Fork.

By definition the most common on my list. Do you remember the old when you used to put listener and watchers everywhere on your angular 1 application… well kind of 🤣.

This pattern is mostly used to trigger a process after an action is dispatched, yeah! like a listener
/* this is the saga you are going to register */
export function* aListenerOnlySaga() {
const somePossibleData = yield take('SOME_ACTION')
yield fork(someOtherSagaProcess)
}
function* someOtherSagaProcess() {
/* Any process calculation you need to do */
}

The use case

Literally, a lot… but let’s keep it real, in our application we need to support different branches/states that requires to display information and take actions based on information based on the current selection.

As Martha, who is a 45 years old secretary who don’t like technology, I need to be able to select a branch from a dropdown and by magic query the information related to it.
/* Some ugly react component*/
class CompanyDropDown extends React.Component {
state = {
company: null,
branches: [],
}
componentDidUpdate ({company, branches}) {
this.setState(({company}) => ({company, branches}))
}
onChangeCompany (company) {
this.props.dispatch('company_change', company)
}
render () { /* omitted for convenience */}
}
const mapStateToProps = ({company, branches}) => ({company, branches})
export connect(mapStateToProps)(CompanyDropDown)
Some details like the render method and the reducers will be omitted to go directly to the point.
/* somewhere in your code... */
export function* listenForChangeCompany() {
/* this variable holds the argument passed */
const company = yield take('company_change')
yield fork(changeCompanySaga, company)
}
function* changeCompanySaga(company) {
const branchesPerCompany = yield call(getBranchesByCompany, company)
yield put({
type: 'company_change_success',
payload: branchesPerCompany,
})
}

Now our UI is separate from out business logic, we are happy 👌🏽. We will add more complexity to this later.

The main benefit of this is that you can create a process catalog (more about this later) that isolates that specific functionality and exposes it at your discretion for your team.

There is a little problem with that pattern, if you noticed, this will work only once, after you exec the process, it won’t work anymore, that’s where the next pattern comes handy.

Watch and Fork.

One of the problems with the Take and Fork pattern is that we limit the amount of executions to only one, as you can see, that previous use case probably doesn’t match the use for the pattern, I made it on purpose, this way we can keep enhancing and powering it is as much as needed, step by step.

Probably a better fit-for-purpose case would be a login or logout processes, where you know that you only need them once.

Moving on with the case, we need to support that our friend Martha needs to change between companies as much as need it, not only once, we can solve this with a small tweak, let’s see the watch and fork pattern in place, let’s bring our listener saga to the game again.

export function* listenForChangeCompany() {
while (true) {
const company = yield take('company_change')
yield fork(changeCompanySaga, company)
}
} /* eh viola! */

Pretty neat eh? if you are not used to function generators, to have a while/true around probably looks weird, but it fits the purpose, however, there is a even better way to do it, we can iterate more over this using another library helper which is shortcut for it.

/* Where you register the sagas */
function* rootSagas () {
yield [
takeEvery('company_change', changeCompanySaga)
]
}

Behind the scenes the company argument is passed to the changeCompanySaga saga. I really like this pattern, specially if you need to handle a big applications with hounders of processes, you just know the responds to a single dispatched action.

Put and Take.

This pattern is useful as I mentioned before, you organize your process operations into different saga and create a services catalog that we can share cross all the team/people/units you name, this means, each of your services has a finite functionally, that will change your state, sometimes that is enough, sometime you want to extends the capability of a single service, let’s see a use case.

Imagine that one of the teams in your company tells you, hey! we created this very complex service that you can re-use, just and it’s called...fetchDataOverFiveDifferentLocations this is a lot of imperative stuff but at the end you will have all the information you need parsed and ready to be consumed. Awesome!

You agreed with your team some actions naming conventions that go as follows {service_name}_{microservice}_{status}, let’s say:

  • fetchSomeData_events This will start the saga.
  • fetchSomeData_events_start This action is dispatched by the service as soon it starts.
  • fetchSomeData_events_success This action is dispatched by the service when it finishes.
  • fetchSomeData_events_error This action is dispatched if there is an error during the process.

This means our services library exposes a saga which looks like this.

export function* fetchDataOverFiveDifferentLocations() {
while (true) {

yield put({type: 'fetchSomeData_events_start'})
/*
computing stuff...
*/
yield put({type: 'fetchSomeData_events_success'})

}
}

On your application you can consume the service like this:

function* rootSagas () {
yield [
takeEvery('fetchSomeData_events', fetchDataOverFiveDifferentLocations)
]
}

What if we need to extend that functionality?

/* We create a manager saga */
function* fetchDataManager () {
/* we need to start the service/saga */
yield put({type: 'fetchSomeData_events'})
/* we need to wait/listen when it ends...*/
yield take('fetchSomeData_events_success')
/*
fork another process,
query info from the state,
do imperative stuff,
whatever you need to do when the previous saga finishes, the sky is the limit...
*/
}
/* We create an orchestrator saga */
function* orchestratorSaga () {
while (true) {
yield fork(fetchDataOverFiveDifferentLocations)
}
}
/* your root saga then looks like this */
function* rootSagas () {
yield [
takeEvery('other_action_trigger', orchestratorSaga),
]
}

Probably some of you are thinking… what about the error handling? hold your thoughts, I will come back to this later.

For of Collection

This one is picky, because most of the time we do not solve the problem this way by default, but when you need it, you need it.

Let’s say that we fetch a collection from any source, we receive 100 objects and we need to apply an operation/service per each, in short words, we need to dispatch one or multiple actions per each element. Normally, this is something you can manage in a reducer, but let’s keep the spirit of the service catalog.

The problem is that when you are in a saga you cannot do something like:

function* someSagaName() {
/* code omitted for convenience */
const events = yield call(fetchEvents)
events.map((event) => {
/* this is syntactically invalid */
yield put({type: 'some_action', payload: event})
})
}

This is when the for of loop comes to the rescue, let’s solve this problem before start breaking our architectural services rules 👮🏽.

function* someSagaName() {
/* code omitted for convenience */
const events = yield call(fetchEvents)
for (event of events) {
yield put({type: 'some_action', payload: event}) /* 💁🏽 */
/* or maybe something like: */
yield fork(someOtherSagaOrService, event) /* 🙌🏼 */
}
}

The way of the for/of loop works is out of the scope of this post but you can find out more here, also, it’s perfectly possible to do it using a regular for loop an iterate over the array, it’s your call.

Error Handling

oh yes! javascript is not Elixir, we still need to do defensive programming and protect from errors 👩🏼‍🚒, based on this catalog structure, how do we ensure that we don’t swallow the errors? or how can each error be managed correctly? it’s not the same a 500 than a 401, we still need a flexible way to communicate the user in a friendly way that something went wrong.

The rules of thumb we use are simple:

  • All Errors are handled inside the sagas.
  • The saga that manages the process is in charge of handling the error.

Let’s go back to our event manager service:

  1. This service is generic.
  2. If the service handles the error, we cannot make any custom error, we are just coupled to the convention error.
  3. If we need to make a custom handler, we need to create a service that handles for the error.
/* Case 1, service that manage the error */
export function* fetchDataOverFiveDifferentLocations() {
try {
while (true) {

yield put({type: 'fetchSomeData_events_start'})
/*
computing stuff...
*/
yield put({type: 'fetchSomeData_events_success'})

}
} catch (error) {
yield put({type: 'fetchSomeData_events_error', error})
}
}

In this case, we are coupled to the service error, so we need to create a service that listens for that action:

function* rootSagas () {
yield [
takeEvery('fetchSomeData_events_error', yourErrorHandlerService),
/* ... */,
]
}

The only con for this that I’ve found so far is how verbose it is to handle a single error, but also gives you a lot of flexibility, you decide on your reducers if you want to react to this error or not, the important part is that it was catched and your application is being notified.

/* Case 2, manager takes care of the error */
function* fetchDataOverFiveDifferentLocations() {
while (true) {

yield put({type: 'fetchSomeData_events_start'})
/*
computing stuff...
*/
yield put({type: 'fetchSomeData_events_success'})

}
}
function* fetchDataManager () {
try {
yield put('fetchSomeData_events')
/*...*/
yield take('fetchSomeData_success')
} catch (error) {
yield put('some_custom_error_action', error)
}
}

You can then handle the error for sample, via a reducer, probably it’s a boolean the only thing you need, that is up to you, both ways work really good, it will depend on your cases and you agreements with the team, remember convention over configuration is the key.

Conclusion

As you might see, this library comes really handy when you need a solid way to share architectural practices cross teams or maybe just create a very descriptive service layer, and most important is really easy to extend to others.

Do you use any other patterns? I’m always happy to learn what others are doing out there and how we can learn from each other. Please let me know!


Finally feel free to check our open source projects at the moment: