Redux-saga style-guide

· erock's devlog

Recommended rules for building web apps with redux-saga

Redux wrote a style guide that attempts to set some standard practices on how to organize state management using redux. I think this is an important step to create a platform for further discussions. I found myself agreeing with most of the recommendations, but there are a few that I disagree with. I think the primary reason why I disagree with the style-guide is because virtually every project I use is built with redux-saga. From my perspective, redux-thunk is rarely the right choice except for very small react applications. Neither arguments building a redux app are wrong, they are looking at redux from different ways to manage side-effects.

As a software engineer that builds a lot of front-end apps with other engineers, it's vitally important that when we build an app, we make it readable and maintainable. Official recommendations are the north star for software engineers because it is the culmination of years of experience and it sets a baseline for how to build an app successfully. Any strong recommendations from the official style-guide requires an equally strong reason for going against it.

In this article I will go through the recommendations that I think are contentious and I want to put forth some recommendations when using redux-saga.

Critique #

Put as much logic as possible in reducers #

I built a large app that originally placed a lot of logic inside reducers. This resulted in spaghetti code that was difficult to maintain:

In general, I think having reducers hold a lot of logic makes that logic easy to test but difficult to maintain, generalize, and refactor over time.

Model actions as events not setters #

Theoretically I agree with this one because it does make the event log easier to read. It will also make time travel debugging -- something I don't find useful -- easier to perform because there would be fewer actions dispatched and they are more traceable to find what triggered them.

However, in practice, I take a hybrid approach. When actions are being dispatched from react, use events. When actions are dispatched from sagas (effects), use setters. This will make an action traceable (find the source of where the action was called) and reducers become generic containers of data that are maintainable, composable. This is how I view reducers: they don't hold logic, they are a database table.

Thinking of reducer slices as database tables provides clarity and consistency to our state management. This doesn't diminish the utility of redux it's just that there are better layers/tools to manage business logic -- hint where we manage side-effects.

Allow many reducers to respond to the same action #

My hot take is that I think there should be a 1:1 mapping between actions and reducers. Reducers should own the actions (via createSlice) and only under rare exceptions should a reducer listen to outside actions.

Employing this method, redux becomes a very thin layer of setters to hold data and tell react when state has changed. I know this point of view is controversial and normally when it comes to building apps as part of a team I don't like to go against the standards, but this really is being driven by my experience building large scale web applications with a team of engineers.

To me, the real value of redux is:

Avoid dispatching many actions sequentially #

I want a list of steps that demonstrate how redux is being updated ideally in the same function. What I don't want is to grep all the reducers for an action type to see how my state is being updated. This is especially annoying when employing a modular, feature-based folder structure. We have replaced a single function that centralizes business logic into a composition of functions that are scattered throughout the codebase. The logic is broken up which makes the flow of what is happening harder to understand. What compounds this even worse, with libraries like redux-saga, sagas can also listen for those actions and activate even more side-effects.

Aside: I try to only let sagas listen for events (react-side), not my setters to avoid errant infinite loops.

The counter-argument regularly cited is that sequential dispatches could trigger multiple react re-renders. This is because each action sequentially hits the root reducer and could return a new version of the state, triggering an update event. redux could have allowed for an array of actions to be dispatched, but that was ultimately rejected. Because of this, developers are now required to use redux-batched-actions. I think it should have been part of the API and if I were rebuilding redux from scratch, it would be an included feature, but I also agree with their perspective: it could make a lot of people unhappy and there's a user-land library that makes it work all the same. Regardless, this suggestion and argument revolving around performance is really because the redux API does not support dispatching an array of actions. If I could add one thing to the redux API it would probably be that.

Saga style-guide #

Take the redux style-guide, remove the ones listed above, and add these for my unofficial redux-saga style-guide.

Effects as the central processing unit #

Most of my arguments revolve around using effects as the primary location for business logic. Whenever I build a react/redux app, beyond the simplest of them, I need something more powerful and maintainable than redux-thunk. redux-toolkit endorses using redux-thunk and only under special circumstances should we reach for something more powerful like redux-saga. Personally, I think this should be the opposite. I understand that redux-thunk is a simple addition (you could inline it easily) with only 14 lines of code but that's kind of my point. Redux has always struggled with one of the most important parts of building a web app: side-effects. To be honest I actually think this is a positive for redux because it manages state, not side-effects. Use another tool to solve side-effects. Even Dan admits that he was hoping that redux-thunk would be replaced by something built by the community.

To me, there's no real debate: use redux-saga. I understand why it cannot be officially sanctioned: because for simple todo apps -- something the js community uses as a litmus test to compare implementations -- it is unnecessary. I get it, but beyond anything simple, you need something more powerful.

Yes, there's a learning curve, the same can be said for redux and yet we all still recommend it. redux-saga uses ES6 generators, they are not that difficult to grok, and are part of the language. If you are an engineer, it is your responsibility to learn all language features.

Reducers as setters #

Redux is an object that should be thought of like a database. Reducer slices are database tables. We should reduce boilerplate with slice helpers (robodux) by leveraging new officially sanctioned helpers like createSlice.

We don't even need to test our reducers anymore because these libraries already did that for us.

This makes reducers predictable, isn't that one of the taglines for redux? A predictable state container? Reducers are simplified, and slice helpers cover 90% of our use cases, because we are treating them like database tables.

UI dispatches events, effects dispatch events and setters #

When react dispatches actions, it should dispatch events, like the redux style-guide recommends.

When effects like sagas dispatch actions, it can dispatch events and setters. This still provides some traceability and helps centralize business logic into one layer.

Avoid listening to setters #

When you ever have the urge to listen to a setter action in redux-saga, think again. This invariably will have the unintended consequence of creating infinite loops where a saga (A) will keep dispatching setters and saga (B) will listen for those setters and dispatch actions that trigger saga (A).

Build indexes for your db tables #

Need to have a sorted list of entities that come from an API? Need to group a subset of entities? First try to create a selector for it. If we need to preserve the order the API sent us then we can create a reducer EntityId[] that acts like an index.

Yes, it feels like we are rebuilding a database, but it's not that much work and the manual process allows for performance tweaking which is desirable when building a large application. You will also have to maintain both reducers together. This might sound tedious or prone to errors but in reality these two reducers are coupled by our effects, so it's not that difficult.

 1import { call, put } from 'redux-saga/effects';
 2import { createTable, createAssign } from 'robodux';
 3import { batchActions } from 'redux-batched-actions';
 4
 5interface Article = {
 6  title: string;
 7  post: string;
 8  author: string;
 9}
10
11interface ArticleMap {
12  [key: string]: Article;
13}
14
15// hashmap to store all articles for easy id lookup
16const articles = createTable<ArticleMap>({ name: 'articles' });
17const { set: setArticles } = articles.actions;
18
19// sorted array of article ids that we receive from the API
20const articleOrder = createAssign<string[]>({
21  name: 'articleOrder',
22});
23const { set: setArticleOrder } = articleOrder.actions;
24
25function* onFetchArticles() {
26  const response = yield call(fetch, '/articles');
27  const articles: Article[] = yield call([response, 'json']);
28
29  // preserve order from API
30  const articleOrder = articles.map((article) => article.id);
31
32  // build hashmap of articles (normalize) for easier
33  // lookup and update
34  const articleMap = articles
35    .reduce<ArticleMap>((acc, article) => {
36      acc[article.id] = article;
37      return acc;
38    }, {});
39
40  yield put(
41    batchActions([
42      setArticles(articleMap),
43      setArticleOrder(articleOrder),
44    ]),
45  );
46}

Conclusion #

These are all ideas I use when building large scale web applications and it has worked extremely well for us. These recommendations are subtle differences between the official style guide and using redux-saga.

Congrats! you made it to the end of this article. Do you love redux-saga? Try out starfx which is our rtk-query/react-query equivalent. Seriously, it is awesome.

I'd love to read your thoughts so feel free to email me (blog [at] erock.io) about this style-guide.


I have no idea what I'm doing. Subscribe to my rss feed to read more posts.