I am about to criticize some beloved state management solutions, and this is bound to upset some people. Great people have worked hard on these, and I am seriously grateful to anyone who has contributed to the many state management options out there. However, I have been asked to clarify why I created StateAdapt instead of using an existing solution.
Before you read this you should read my article introducing StateAdapt. It is much more positive than this.
Anyway, let's get into it.
NgRx
State pattern is coupled to state
Recently I created a complex tree of components in an NgRx project only to find out later that a 2nd copy of that tree of components needed to exist on the same page at the same time. I would need to add an extra property to 30 actions so that the reducer could become more complicated and know whether an action should apply to one place in the state tree or another.
If we managed state like we built UIs—with components—we could add another instance of a state pattern in as little as 2 lines of code. I believe we could achieve this with the state adapter pattern introduced in NgRx/Entity, but I think most people would view it as even more boilerplate unless the surrounding boilerplate was reduced at the same time. That is what I did in StateAdapt.
Boilerplate
Everyone knows about the boilerplate problem with NgRx. The NgRx Core team has done a terrific job of reducing this, but in order to reduce it to a minimum you would need to take full advantage of RxJS, which StateAdapt does. StateAdapt achieves the same benefits as NgRx with 60% the minimum code compared to modern NgRx.
Best practices?
NgRx seems to try to distance itself from Redux, but it is still a copy of Redux wrapped in an observable. Unfortunately, there is a lot of wisdom and best practices in the Redux community that did not successfully make its way over. There is some guidance, like this terrific talk by Mike Ryan, but I believe people are largely using NgRx in a way that minimizes the benefits they get from it.
I wrote Stop using ngrx/effects for that in 2017 in the hopes that people would stop using ngrx/effects for everything. It mostly didn't work. The first example in the NgRx/Effects documentation is for something that NgRx/Effects shouldn't be used for: Subscribing to data by dispatching an action. In my article I explain why using plain RxJS makes data dependencies much more flexible and maintainable.
My solution did not catch on very well, and I have seen indications that people were intimidated by the little-known using
function from RxJS that it requires. Unfortunately I was too lazy to write more about that solution. So, one of my primary goals in StateAdapt was to just take care of that behind the scenes. I am very happy with how it turned out. With StateAdapt, developers can focus on thinking reactively and not worry about how to involve the store.
NGXS
State pattern is coupled to state
This is the same problem I talked about with NgRx above.
Everything is an effect
I already thought people were drastically overusing NgRx/Effects, but then somebody created a state management library where literally everything is an effect.
Rather than repeat myself about why I love pure functions, I will just refer you to my first article. I think it is wonderful that people are trying to reduce state management boilerplate, but I personally find NGXS code to be hard to understand. Data structure manipulation gets in the way of trying to understand the overall data flow in an application, and vice versa. I like separation of concerns between the big picture stuff and the details of how state is changed. NgRx has that (when not overusing NgRx/Effects) but it comes at the cost of all that boilerplate. But StateAdapt maintains it while reducing boilerplate.
Multiple action dispatching
Dispatching multiple actions at the same time is an anti-pattern in Redux, and NGXS encourages it with special syntax.
One of my first experiences in an NGXS application was opening Redux Devtools and seeing this:
I looked at the code and saw that these actions were all dispatched at the same time.
The line between what was happening and how the app reacted was blurred to the point where I could no longer clearly see what was happening. There should have just been one action dispatched: AnswerCall
. If I was interested in the details, I could click on it and look at the state changes.
Also, if you try to jump to any of these actions in Redux Devtools it will put the app in an intermediate state that is impossible in reality.
The last issue is that this is backwards from FRP. Rather than multiple action handlers listening to one action in their respective state files, the event source is in charge of making all the changes downstream. An event source that dispatches multiple actions is a lot more than an event source.
Akita
Imperative State Management
State is updated imperatively in Akita, like it tends to be in NGXS. Actually, NGXS would be improved if its syntax were more like Akita's, but they both suffer from the issues I mentioned in the section about NGXS.
Subjects in a Service
No Redux Devtools
RxJS is notoriously annoying to debug. Usually you have to edit the file by putting a tap(console.log)
in there, reload the app and reproduce the situation you wanted to debug. But Redux Devtools keeps track of everything automatically, so you can just open it anytime and explore what happened at any time.
(Actions and state reactions are a big part of understanding what is happening in an application, so Redux Devtools is great, but I am still trying to find a better way to debug RxJS for the other parts of applications. If anyone knows of anything, let me know.)
No Selectors
Sometimes you want to combine observables, but combineLatest
emits once for each input observable, even if the input observables emit synchronously. I believe this is the original reason NgRx included createSelector
, because selectors solve this.
It is also really nice to have all the derived state calculated in pure functions, separate from the asynchronous RxJS stuff.
Another benefit is with derived state. Pure RxJS makes you map
, distinctUntilChanged
and shareReplay
if you want to calculate derived state efficiently. Selectors do not need any of that.
StateAdapt
So, all of that is why I wrote StateAdapt. Is it perfect? No. Is it for everyone? Well, I think it is for everyone who loves minimal, reactive, debuggable and reusable code.
Give it a try and let me know what you think.
Others
If there are any I missed, please let me know. I searched a lot before I wrote StateAdapt, but I may have missed something.
Also, if anyone wants to see any comparisons, I would love to create some. I already plan on doing some. But if you show me a feature developed in one state pattern, I will recreate it using StateAdapt.
Thank you!
And please forgive me! I love you all!
Top comments (18)
Is it ready to use in production?
And what about updating repo?
I'm currently helping a team upgrade from AngularJS to Angular and haven't had time recently, but in the next couple of weeks I will be getting back to StateAdapt, and I should have several months to work on it and write about it. I was about to release documentation for the adapter library and a couple of adapters to start with, and then version 1.0 shortly after that. Nothing fundamental will be changing though I don't think, so if you feel like using it in production, I don't see an issue with it. Check out the website to make sure you've got the core concepts down and let me know if you have questions.
Two years later, I'm still looking for version 1.0 StateAdapt :D
Yeah...
I'm releasing an article in 16 hours explaining the most recent bug I found. It almost caused a fundamental API change, so I'm glad I found it before 1.0.
But I've implemented it in 20-30 projects now and feel really good about it after this last bugfix. I have a bunch of unit tests covering everything I've found. I haven't used in in a humongous project yet, but I've used it in medium-sized projects and my confidence in it is pretty high. The last project I did was what made me discover the last major bug I think, so doing these projects has been valuable.
Here's the last official issue required for 1.0 github.com/state-adapt/state-adapt...
But my unofficial todo list currently looks like this
It will probably take an extra week, and I'm thinking of waiting for Angular 15 to release 1.0 as well, but not sure on that. So, I'll say either 11/22 or when Angular 15 comes out.
The Angular library is ready to use in production I think.
1.0 is only delayed now because I already had a React implementation and wanted to test it for 1.0 to release at the same time as the Angular library.
Just make sure @state-adapt/core @state-adapt/rxjs @state-adapt/angular are all on the same version. There was an issue with ng-packagr that caused one of them to have a stupid peer dependency. node.js 14 had an issue with it with npm install, but I think everything else is fine.
Congratulations, Can you please make an example of how to use it without zonejs like rx-angular.
Rx-angular has template library and it's missing in other, is it worth to use both stateAdapt and rx-angular template together?
Yes, use them both. No reason to re-implement RxAngular's innovations for templates.
This is an example of Todo MVC that I forked from the RxAngular implementation, which I believe is zoneless. It's a 3-month old example, but I don't think any API I used has changed. github.com/mfp22/state-adapt-todo-mvc
The next couple of days will be finishing up documentation. I'll comment again when the new docs site is live.
And here's a diff between the RxState and StateAdapt implementations
github.com/edbzn/rx-angular-todo-m...
I run it, new todo does not added to the list
Any console errors?
I'm sorry It was my mistake, It's working right.
Was looking at the NgRx example (stackblitz.com/edit/state-adapt-ngrx)... Can you explain what the benefit of state-adapt is in comparison to the native NgRx implementation?:
countAdapter
looks like a nicer reducer... it's cool that the reducer is just an Object. (stackblitz.com/edit/state-adapt-ng...)Instead of creating NgRx actions and dispatching them to one action stream you create many "Sources" and use
next
on them.state-adapt looks like having just another syntax for actions, dispatching actions and reducers. Am I missing something?
I played little bit with the example and found it a bit tedious to repeat e.g. the
increment
key insidethis.adapt.initGet
andcreateAdapter
.At least that given NgRx example does not look like less boilerplate to me... you still have some kind of actions (Sources) and dispatch them (Source.next) and some kind of reducer (the createAdapter Object).
Hey thanks for checking it out!
I actually think of the object here as the reducer:
If you find it tedious to repeat
increment
then it seems like you would very much love something that is "just another syntax" if it was minimal. StateAdapt is 60% the non-business logic code in NgRx and I bet similar for NGXS. Which one of these looks more tedious? NgRx on the left, StateAdapt on the right:(If you want to read more about that example it is from my last article).
I think minimalism is important, but I took it as far as I could before it would take value away from the pattern itself. The decoupling of state patterns from specific instances of state makes it worth repeating things like
increment
. It would be like complaining that with components you need to define an input in one place@Input() data: Data;
and then typedata
again when you use the component:<app-component [data]="data"></app-component>
. That isn't tedious, is it? Because you know that the component is reusable. Do you not see value in the reuse of state adapters?It does a couple of things that are impossible in NgRx, but they don't matter very much to me. I don't see myself instantiating state dynamically inside components inside an *ngFor, for example. Maybe somebody will do it though.
When I say "boilerplate" I am not referring to separation of concerns. Those are good. I am referring to all the extra syntax where you want schematics to help you pretend that Angular isn't making you write and manage non-DRY code. StateAdapt has extremely little boilerplate.
I think it's fantastic that you've been trying to make state management easier and I really like what you're doing. I found NGXS to be much easier than NgRx and the concept count much lower. Less boilerplate and less RxJs code, which is good for most people, especially newer devs. I would say that what you've created looks really similar to some plugins I've tried in NGXS. Namely github.com/ngxs-labs/emitter
I actually don't think managing deep state trees is that difficult if you use State Operators: ngxs.io/advanced/operators#state-o....
I also think that having actions be asynchronous is a good thing, not a bad thing. You have less side effects this way (at least how I understand it).
I just found this article as well: dev.to/this-is-angular/why-and-how... and this looks very promising. Forms is a PITA and needs to be solved better in angular. Maybe state adapt could solve forms in a better way!
I will also say NGXS has another great plugin that allows you to separate your action handlers and move them to a simple more unit testable pure function, so that certainly helps with being more DRY and could be easier to update state.
My last comment would be that I know NGXS has some interesting things in the works, like local state.
I think the main difference between my philosophy and that of NGXS is I actually think the more RxJS code, the better. I believe it achieves much better separation of concerns. I believe reactive programming is the future of web development, and a lot of the friction we are feeling now is more from our old habits and ways of thinking than any inherent conceptual difficulty with RxJS. The first thing most people do with a click event is write a callback function, and that's the first sign they're thinking imperatively still. It's no wonder they think reactive programming is extra, because they are not letting go of imperative muscle-memory so they have to carry both mindsets at the same time while programming. Whichever is the new thing will take the blame for being harder.
I think of boilerplate as stuff that could be generated with schematics. Actually, if you can generate code, it's probably not DRY. I think NGXS reduces new concepts but not necessarily boilerplate, since there is still a pull towards schematics. But it is just subjective as to whether you see value in reactive style vs imperative, so what is boilerplate and what is valuable code is subjective as well.
I am unfamiliar with many NGXS plugins. I do not think that plugin is very similar to my thing though. It looks like the event source has the responsibility to know what state is supposed to change.
I look forward to seeing how NGXS progresses over time. Since I think FRP is basically inevitable, I imagine we will see NGXS plugins that rely more and more on RxJS. I could be wrong, but I hope I'm not :) My personal experience is just that life is much simpler in the reactive paradigm once you cross :)
Edit: Oh, and thanks for commenting! I really appreciate hearing your thoughts.
I completely agree about reactive programming. That's one of the reasons I got away from React. Hilariously named since it isn't in fact reactive...
I love RxJs and feel really comfortable with it. I just have seen too many people overuse and abuse it. For example, using a combination of several operators in one stream when it turns out you could use a couple different subjects and a couple of operators. Stuff that takes a lot of experience to learn.
If you have some slightly larger examples of using StateAdapt I would love to see them. The biggest take away I have from looking at StateAdapt is easy state changes, not changing large state trees. Does that seem right to you?
React does not react very precisely, and when you involve asynchronicity you move into an imperative style. Sometimes I would want to call it OverReact, other times AlmostReact. Still, when you add RxJS it can be nice with hooks and not having to deal with
| async
.I think StateAdapt shines most with large state trees, actually. You can use the same source and also select across several mini-stores, so I see no issue in scaling the pattern at all. You can define it in a service and reuse it anywhere, too. It just feels more like local state, because you are not describing a giant, global tree structure up front. But even in NgRx you treat each slice of state independently, so for the sake of devtools it seems unnecessary to me to force everyone to think about a global object at all while developing. Behind the scenes it get combined into one, but that is for dev tool purposes and shouldn't affect the development experience.
You are right, I need some less trivial examples. I think the next one I do might be something like a shopping cart demo, where you have filters, a filtered list, and a list of items in a cart. Something like that. I wish I could convert a large, real-world project to StateAdapt and write a big post about it, but it would need to be publicly visible, so none of the large projects I've worked on would work. But I think if I write enough small-medium examples, they can add up.
Thanks for sharing. I am exploring alternative state management solutions to NgRx and I would definitely take a look at your library.
Just out of curiosity, do you happen to come across
rx-angular/state
? I haven't done any in-depth exploration yet but looking at the basic demos, it does feel kinda similar. Correct me if I am wrong and would appreciate your insights. Thanks!