In recent years I’ve observed a strong trend towards using reducers in software architectures. While this is not a new pattern, its recent prominence in React / Redux and other platforms offer benefits to software quality worth discussing.
In this article I’ll go over the role of reducer functions in state management and talk about some of the main benefits it offers. Finally, I’ll close by discussing some tradeoffs I’ve seen in Reducer-centric architectures.
I’m going to leave discussion of Model View Update (MVU) to a future article in order to keep this article narrowly constrained to the idea of using reducer functions for state management.
Let’s look at an example reducer from an Angular NgRx application:
And another from an F# Elmish.WPF application:
Both of these examples illustrate various flavors of reducers, but both take in a starting state and an action and return a single new version of that state.
At it’s core, this is what a reducer does.
In this article, we’ll explore what’s so powerful about this and the problems this helps solve.
Meeting Reducers for the First Time
A few years ago I was doing some very heavy single page application (SPA) development in Angular. I built an Angular Single Page Application. And then another. And another.
I loved the framework (and still do), but pretty soon I began to notice difficult to manage complexity when working with multiple asynchronous operations at once.
This is a point that many in the Angular community have gotten before where rare state management bugs emerge and the order of operations and network latency can introduce a large degree of complexity.
The Angular community rose to the challenge with some reducer-based state management libraries like NgRx. This library was based off of the popular Redux state management library commonly associated with React.
Based on what I now know about reducers, the shift in the Angular community to use reducer-based state management systems once state management hits a certain complexity threshold is the right one.
Power to the Reducer
Let’s take a look at why reducers are so good for software quality.
Pure State Transformations
Instead of relying on repository classes that hold ever-changing state values, reducers are pure functions that take in an action and a previous state and output a new state based on those inputs.
The term pure function means that the function can be called forever with the same inputs and always return the same output without having side effects on anything else.
This concept is extremely important to understanding the quality benefits of a reducer function.
Because a reducer is all about repeatable state transformations given specific inputs, it is incredibly easy to test.
Centralized State Management
An interesting aspect of reducers is that it puts all application state in one centralized place.
This makes it easier to look at the entire state of the application, but more importantly, it moves any manipulation of the application state into a central place. This removes the doubt as to what parts of your application are modifying your state.
This improvement is very important because a surprising number of bugs arise from inconsistent behavior in state management.
On top of that, if a bug arises, all you need to know is the inputs to the reducer function in order to be able to recreate and resolve the issue.
By logging state before and after reducer functions, debugging is exponentially quicker in scenarios where you are uncertain which operation resulted in arriving in an invalid state.
Note here that I’m not advocating for a specific reducer-based library or technology, but more the pattern in general.
If you’ve been around in the technology world for awhile, you know that any decision has pros and cons associated with it. I can’t advocate for reducers without discussing common pitfalls and drawbacks associated with them.
Let’s take a look at those potential drawbacks now:
- Learning Curve – Reducers are a little different and have a mild learning curve associated with them – particularly when starting a new project without patterns in place to emulate.
- Boiler Plate Code – Many reducer-based frameworks I’ve looked at have at least a little bit of what I would call boiler plate code. This is code that has little reason for existence other than the framework requires it. It’s hard to get into this without looking at a specific implementation, so just know that you may need to write some repetitive code to integrate reducers into an existing framework.
- Complexity – Given the complexity and overhead of reducers, they don’t necessarily make sense for small applications or applications that don’t rely much on state manipulation. Just like you don’t need a moving truck to go to the grocery store, reducers don’t always make sense in small applications.
- Large Reducers – If your reducer grows to a point where it has a lot of potential state operations, it can become a fairly large method. There are answers to this like extracting methods out for complex transformations (which I recommend).
Additionally, depending on the flavor of reducer framework you’re using, you can have multiple reducers or nested reducers. This makes things a bit more complex, but also keeps methods small and manageable.
Overall, I am pleased with the shift towards reducers in recent years. Their construction and design makes it hard for bugs to hide.
The main drawbacks I’m seeing are around the initial mild learning curve, a rise in boiler-plate code associated with reducer-based frameworks, and added complexity.
Stay tuned as I talk about the reducer’s role in Model View Update (MVU) frameworks in future articles.