It's finally time to pick a state management library. I implemented my colors app in each state management library to see which one best supports the "progressive reactivity" pattern described in this series.
We'll use 3 criteria:
- Imperative Code (less = better, more reactive)
- Lines of Code (less = better, more progressive syntax)
- Popularity (more = better, more stable)
My implementations are a little unusual for each library. For example, NGXS supports many imperative patterns that are commonly used, and I didn't use any of them. I also created some utilities to enable the most reactive code possible for each library. I will discuss specifics in the final 3 articles in this series, which I will publish soon.
First, let's look at the results.
Here are the minimum number of imperative statements for each state management library at the highest level of complexity in my colors app:
|Imperative Statement Count
|Imperative Statement Count with Reactive Utils
|Subjects in a Service
The minimum number of imperative statements possible with Angular is 4. Unsurprisingly, StateAdapt achieved maximum reactivity since it was designed exactly for this purpose.
RxAngular is the 2nd most reactive out of the box with 5. This isn't surprising because it was created by Michael "Rx" Hladky.
But standard NgRx/store is also very reactive with only 7 imperative statements. Why is that? And why are the others less reactive?
NgRx/Store was inspired by Redux, which was inspired by Flux. What does this have to do with reactivity or declarative code? From the presentation that introduced Flux to the world 8 years ago:
"We have an external handler that goes in and changes some of their state. Each have their own state, but the [event] handler goes in and modifies them. All of the logic to modify state based on a new [event] is in the handler itself. And so those 3 pieces no longer have the ability to maintain internal consistency. They basically don't have that information. They've already ceded that control to the handler. What we want to do instead is internalize this control. We want to move all the control into the individual pieces, so that the state is right next to the logic that updates that state."
This is exactly describing declarative programming (see Rule 1). They called it "unidirectional data flow" and "single source of truth," but these are just other ways of saying "reactive" and "declarative." The beginning of all of this state management chaos—all of these libraries, all of it—was an attempt to write declarative async code.
The negative reactions to Flux, Redux and NgRx were of 2 kinds: Resistance to the massive boilerplate (actions, dispatchers, reducers, "thunks", etc); and resistance to reactivity itself.
RxJS wasn't popular yet, so it's hard to blame Flux for not having syntax as nice as RxJS. But the resistance to the verbose syntax made sense: It takes longer to build everything, it obscures business logic, and it's like molasses when refactoring.
The resistance to thinking reactively comes from lack of experience. Whether a front-end developer has been programming for 1 year or 10 years, experiencing the pain of slow progress caused by the explosion of complexity from out-of-context imperative updates is what gives appreciation for the sanity achieved only through declarative code.
Here are the total lines of code for each state management library at each level of complexity in my colors app:
However, after creating reactive utilities for each library, I was able to reduce the lines of code by 10-20% for each library:
StateAdapt remained the same, since it was already optimized to be minimal and reactive.
Subjects in a Service (RxJS) is still there, it's just underneath NgRx/Component-Store. It turns out that the code for the store and component classes are identical for those patterns with the reactive utilities.
Here's something interesting: StateAdapt is less than half of the code compared to NGXS. If you already implemented Level 3 complexity in NGXS, the change to accommodate Level 4 complexity (reused state logic) requires so much code that it would actually be less work to delete everything and rewrite Level 4 in StateAdapt. This is an extreme example though, where the business logic is extremely minimal, which is good for illustrating and comparing the state management patterns themselves, but probably does not reflect realities in most actual projects.
|Subjects in a Service
StateAdapt is just getting started, and it should not be used in production. 1.0 will probably be released within a couple of months though. Feel free to give it a star :) StateAdapt
Unfortunately, there is something wrong with every library!
I created StateAdapt to be the perfect solution for progressive reactivity, but it is not ready for production. I couldn't have it ready for production before writing this, because I used this article series to guide my thinking for the syntax.
NgRx/Store is the safest choice, but it has the 2nd highest lines of code, which means its syntax isn't very progressive—if you choose a simpler solution first, and then the feature must become more complex to handle a change in business requirements, what are you going to do? Put it off and create a bigger mess to clean up later while avoiding the big switch over to NgRx? Everything is a syntactic dead end if you end up needing to switch to NgRx/Store, because of how much will need to be rewritten and added.
NGXS is similar, but even more lines of code. It also supports many imperative patterns, so if you use it and want to adhere to the "progressive reactivity" philosophy, make sure your team understands Rule 1 very well.
I would say for NgRx/Store and NGXS, only use them if you have a highly complex app and you plan on using it in every feature that might become complex enough to benefit from selectors and Redux Devtools. For the features that will definitely not become that complex, you could use one of the other 4 popular libraries. But it seems rare to be able to know 100% that a feature will never get more complex.
Subjects in a Service, NgRx/Component-Store, Akita, Elf and RxAngular do not support selectors, and the Redux Devtools support is fairly weak in Akita and Elf (they only show the action type, not the payload/argument). Also, Akita and Elf did not score as high on popularity as the others (except StateAdapt). As a side note, they were created by the same person, and although Akita is still supported, he prefers Elf for many reasons.
So I don't know. I wish I had an answer for you. I had no idea this is the conclusion I would arrive at when I started writing this and diving deep into each state management library. But every single library/pattern has huge issues, including StateAdapt (still pre-1.0, not popular).
But I want to help all Angular developers using any state management library, so in my next articles I will share how I made each state management pattern more minimal and reactive. I would love to see these adopted into the libraries themselves, but we'll see what people's reactions are to them before I pursue anything like that.
I guess I have to come up with some recommendation, so here it is:
- Use NgRx/Store with the reactive utilities I will explain in the next article for every feature that shows any hint of growing to nontrivial complexity.
- Use Angular features and RxAngular for everything else, since RxAngular is fairly popular, very reactive already, and has a lot of reactive utilities
- Use StateAdapt in a side project to help me get it to 1.0 as fast as possible, so we can finally have a state management library that supports 100% reactivity, selectors and Devtools with an adaptive syntax that doesn't require a huge up-front commitment. Open issues if you find any bugs.
Alternatively, if you are already using a state management library, adopt the reactive utilities I describe in the next articles, and your syntax will be as close to the others as possible. This enables an easier lateral movement if you feel the need to switch state management libraries. For example, if you are using Akita, you can adopt the reactive utilities and end up with code that is extremely similar to the other class-based state management solutions. It is also closer to StateAdapt than standard Akita, so when StateAdapt becomes more stable, you can more easily switch to it if you want.
If you're wrong about any of your assumptions (which WILL happen), there isn't much you can do about it—you will just have to deal with either excessive boilerplate if you aimed too high, or opaque, spaghetti RxJS that was never meant to deal with complex synchronous state, if you underestimated complexity.
In the end it is up to you how important you feel progressive reactivity is. I have done my best to persuade you that it creates cleaner code and prevents you from running into syntactic dead ends. Regardless, I hope I laid out the ideas in a way that helps you understand and make a decision appropriate for your project. Think about the results I presented in this article. Let me know what you think. And I will provide more detail about each library in the next 3 articles, including links to a StackBlitz for each.