Abusing redux-saga

Szabolcs Damján
Byborg Engineering
Published in
4 min readDec 18, 2020

--

To a man with a hammer, everything looks like a nail

To a man with a hammer everything looks like a nail
Photo by Hunter Haley on Unsplash

The title is a little bit provocative, isn’t it? Let’s see some of the ways we misuse one of our most valuable tools, the redux-saga library.

Most projects based on this technology stack use “officially recommended” combine-reducer patterns to operate, which, sooner or later, become somewhat limiting.

This pattern recommends splitting up your central reducer into several “slice reducers”. These slices are usually arranged according to the data’s (business) usage by separating the different data domains or topics. This approach has indisputable advantages the slice reducer operates only on a certain segment of data, thereby making the program code simpler. However, this benefit has another price to it. As your application becomes more complex, you will want to start using data from other state-slices as well to calculate your reducer’s results.

A very common workaround to this situation is to simply write a saga-task to access the data that the slice-reducer cannot. Your project will quickly flood with anti-pattern saga-tasks, such as:

  • Listening for an action -> selecting some data from the store -> dispatching other action with the extended payload
  • Selecting (reading) data from the store -> modifying data -> dispatching action with the result

All these processes end with at least one “put” effect creator. The saga engine will later dispatch these asynchronously within the program flow.

By design, this pattern allows for the possibility of race conditions.

You can read more about this topic in my other article: https://medium.com/jasmin-engineering/redux-saga-race-conditions-1e3b6a22e9cd

Let’s check out two possible solutions to overcome this limitation, even though there can be several good approaches depending on the project.

Advanced reducer concept

The official Redux documentation also mentions the case of certain reducers needing more data from the store. They recommend the following:

  • Use custom reducer logic and pass the required data as an extra argument to the reducer function.
  • Extend the action payload with the required extra data in the action generator.

I will discuss the first solution in this article.

We can substitute the “combine-reducer” component with an advanced counterpart. We’ll keep the slice-reducer concept in the sense that the data domains remain separate, but by using an advanced reducer component, all slice-reducers will receive the whole redux-state as a third argument, thereby making it possible to use values from other slices as well.

For example:

This way, all reducer functions in all slice reducers will have access to the whole state. We have eliminated the necessity for saga tasks whose sole purpose is to select data other slice reducers cannot access.

The result is less saga tasks and more “atomic” state updates implemented in pure reducer functions.

Generic reducers with delegation

This subtitle sounds weird or at least meaningless for the first read, let me explain the concept. Imagine an application which handles various entities arranged into several data collections, such as users, user transactions, messages, notifications, etc.

To generalize common data collection operations like create, read, delete, or update in the collection, we can design a generic entity collection and implement them as redux store slices to the corresponding slice reducers. This is a good idea because most of these collections operate almost in the same way. Lacking a batch update function, we can only delete or update the required set of elements one-by-one, resulting in each item having its own separate action dispatch (using the “put” effect on redux-saga). Each dispatched action will be queued under the hood. On one hand, they will be executed asynchronously, on the other, different tasks can also dispatch actions to the queue, regardless if you have “yielded” the desired number of put-effects, or used the “yield all” effect generator, because of redux-saga’s implementation characteristics.

Don’t forget that your saga engine can run several tasks in parallel, so do not be surprised if other actions also appear in your series of actions.

For example, we can observe this order of actions when dispatched by a parallel saga task:

“DELETE, DELETE, DELETE, DELETE, UPDATE_ITEM, DELETE, UPDATE_ITEM, DELETE …”

I don’t think we need to discuss why parallel tasks manipulating the same data records could cause problems.

To avoid this mess, we should try to dispatch only one action, and the reducer will do the rest. A simple solution to generalize batch operations is to pass an iterator function that runs for every item, so the reducer delegates the decision and operation to the iterator function.

I can already hear you say: but, this reducer will return a new collection reference if none of the items were updated, and cause unwanted rendering in React.

Optimize the approach a little bit further. Pass two separate lambda functions to the reducer — the first will help decide which subset needs to be modified, and the second will perform the update on the selected subset.

Filtering which subset needs to be updated in a separate step will help the reducer to determine whether the whole collection should be returned untouched because no operation was necessary.

I hope these ideas will be helpful for a lot of engineers who chose the redux-saga environment for their projects!

--

--