Redux-Saga in Action(s)
You’re doing it all wrong… that’s how these things are supposed to start right?
A little while ago I was hanging out in the #redux channel on the reactiflux discord server musing about saga and reducer interaction.
No one was responding (8:25am people are usually getting into work or waking up or both) so I dismissed the idea while considering the dreaded boilerplate code as a con. After a bit more thought I changed my mind, again…
I thought if I tossed in some more detail it would stick better in my brain. I cranked up the Google machine and started trying to find some more opinions on the subject. I eventually landed on a github issue which was a discussion around the very idea I was trying to conceptualize.
I always found it funny how ideas materialize within a community simultaneously by multiple people who are otherwise disconnected from each other. The Tipping Point by Malcolm Gladwell discusses this a bit and it’s further explained in the wikipedia article Multiple Discovery.
All comments, though slightly different, had a similar theme of using the saga as the center point of communication rather than just your async action creator replacement.
“Because sync actions should be in reducers, and async actions in sagas”… but why? — 3LOK
I’m going to assume you are well versed in react, redux, and redux-saga. This is primarily targeted to devs who just like reading about other dev’s architecture. I make no claim this it’s flawless nor that you’ll reach nirvana if you follow it. It’s simply an explanation of what I do. If you don’t understand all the concepts simply hop on the reactiflux server linked at the top and ask questions. Everyone is very helpful and it’s a really incredible community of intelligent and thoughtful people.
…you’ll reach nirvana…
Out of context ^
The entire premise rests on the idea that there are two types of actions in your application. Actions that are kicked off by user & system events and actions that modify your state through the reducers. People have suggested calling the former signals and the latter messages, but to me those sound too similar. Instead I call my reducer actions deltas since they’re responsible for changing state.
While not exactly definitions, these are the rules that I apply to saga and reducer interactions.
module — a segmentation (folder) of the application that contains all logic and code; similar to the ducks approach.
The key takeaways here are the accepts and executes rules in the reducers and sagas. I’ll attempt to explain the workflow given the following scenario:
You have a list of stock price items containing the ticker name and the value that can be filtered by a sector such as healthcare. There are two modules involved: one for handling the stock items the other for filters.
The first thing we need to do is create the actions to handle these events. I have a couple helper functions to do this for me.
Signals often deal with async requests which inevitably fail so their shape is a bit different than deltas. Here are the stocks and filters actions:
There are now 4 signals that can be triggered by the application bootup or user interaction and 2 deltas that can modify state. This simple separation of responsibility makes it very clear what the intention of each module is and how it affects your workflow and state.
Now that we have our actions, let’s build the reducers to handle the deltas.
Actions: check, Reducers: check, Sagas: .next()
Now we’ll get into the guts of the application. The sagas are the great coordinators of the application. They bridge the boundary lines of modules by knowing when to kick off an api call, trigger a change to state through deltas, initiate another module’s saga through a signal, and start a saga within its own module. I highly recommend watching the video by Caitie McCaffrey on distributed systems and their relation to sagas.
A big piece missing from this article discussed in the video above is how compensating actions should be used in fail states for roll back. That’s an article unto itself so we’ll leave that bit out. Watch the video!
You should have noticed the INIT action in each module. There is a saga in each for initializing that piece of the application with its needed state on app startup. Any additional sagas are added beneath the init function.
Great, the complicated bits are over. Now let’s wire this all up to our UI and claim victory! First the list of stocks in the StockContainer which should be populated since our stock INIT signal was fired on application startup (not shown in this article).
Now the FilterContainer which is also populated through an INIT signal and also contains a bound signal APPLY_FILTER to kick off the saga.
Here’s a crude workflow diagram that may help those that need the big picture.
It sure does look like a lot of moving parts just to filter some data, but I assure you the effort is worth the simple to grok nature of the workflow. Each module contains its own state and workflow logic which allows for greater traceability and testability.
I’m only at the early stages of the project I’m working on where I follow this pattern. There will certainly be gotchas along the way but so far nothing unsurmountable has come up to make me change my ways.
There is definitely a lot of “boilerplate” code that everyone seems to be afraid of, but I actually sort of enjoy it. It’s explicit and offers a very simple mental model when you’re tracking a bug down or coming up with a new feature. Plus you can always build helper functions to eliminate boilerplate.
All the code above was made up on the fly as an example so I don’t have a github project that you can just npm start and go nor was this code tested for syntax errors. I do encourage you to play with the idea yourself and tell me how I’m doing it all wrong.
I’m on reactiflux a lot so forward your complaints to @ totaldis