Level 3: Complex Changes and Derived State
Every one of these libraries leans on RxJS to some degree. Drawing from Part 3 of this series:
Now that we're dealing with RxJS, we can't forget how capable it is. It can take on nearly any task, even ones it probably shouldn't. If we're not vigilant, our seemingly straightforward RxJS pipes will expand until they turn into a tangled mess that makes our coworkers want to vent about RxJS on Twitter.
There's no crisp boundary between "appropriate for RxJS" and "beyond the scope of RxJS." But these clues point to Level 3 complexity:
- You reach for a
tapoperator withconsole.logto troubleshoot a stream. You need devtools.- You use
distinctUntilChanged,shareorcombineLatestalong with other merge operators. You need memoized selectors.- You find yourself wanting to spread objects and arrays in templates to hand to
behaviorSubject$.next(...), or you're drawn to writing methods that mutate those objects from elsewhere. You need a centered place to define every way your complex object/array can shift. RxJS can manage this viascan(), but the boilerplate adds up.So what we're after:
- Devtools
- Selectors
- Centralized, declarative state changes
That combination sounds an awful lot like a state management library.
Level 3 complexity only marks the entry point for these requirements. Moving forward, the more your features expand, the more cumbersome the raw RxJS becomes.
Redux Devtools offers a remarkably useful view into your application's state. Yet among these libraries, only Akita and Elf support it, and even then only with limitations.
None of these libraries ship with selectors, but they do offer helpers that take some of the sting out of RxJS. RxAngular stands out in this regard. (For RxJS "Subjects in a Service" I'm assuming you've established a base class like this one that appends a distinctUntilChanged at the end of a select method.)
What about centralized, declarative state changes?
As it happens, only RxAngular gives you a built-in approach for defining centralized, declarative state changes. While experimenting with it, I noticed how closely it resembled StateAdapt. The one drawback I found: it sets up subscriptions that survive either as long as the component using the state, or indefinitely when placed in a service. That means when you share state across 2+ components through a service, navigating away and back won't re-trigger HTTP requests unless you invoke something from the component — which is why RxAngular ended up with 5 imperative statements instead of 4.
Even with that caveat, RxAngular outperformed the rest by a wide margin. The others remain heavily imperative, each requiring 10 imperative statements total in the most involved example I built. I wouldn't fault anyone for staying with vanilla RxAngular, but the other libraries could seriously benefit from some reactive helpers.
These libraries work fine for straightforward features. But features rarely stay simple, and once they evolve into more complex territory, we wind up wishing we'd chosen something better matched to what the feature has become.
We still have to work with what we picked, though. The reactive utilities I'm about to walk through help in a couple of ways. First, they dial down the imperative code, which translates to clearer, easier-to-maintain logic. But they also push the implementations for each library toward a shape that looks remarkably close to StateAdapt — and even the reactive patterns of NgRx/Store and NGXS covered in the previous article — so migrating complex features to a library with selectors and Redux Devtools becomes less painful.
So here are the 5 recommendations that will anchor the rest of the article.
1. Keep state inside services
A few of these libraries let you have your component class inherit from one of their base classes. I'd steer clear of that. The concern is that you might later want to relocate that state into a service, and that shift will force you to rework the interface you use to access state throughout your template. Starting with a service from the outset avoids that minor syntactic trap.
2. Build your own base class
Rather than extending their base class directly, I'd define your own class that every state service extends (for Elf, a plain class works). This is where the reactive middleware gets inserted. I named it ReactiveStore for every library except Akita, where I went with ReactiveQuery. Here are the links to my implementations:
3. Override (or create) the select method
I'd prefer to skip this step, but it turns out to be essential. When state lives in a service, it persists for the application's lifetime. That means we need a subscription mechanism where HTTP requests fire again automatically when you return to a component that depends on that state. The next recommendation explains part of the mechanics. Full implementations for each library are in the links above.
Overriding select means for RxAngular, Akita and NgRx/Component-Store you'll occasionally need to refresh the TypeScript method overloads to stay aligned with library updates. Just navigate to the definition, copy their overload types above your select implementation, and pull in whatever extra types you need from their package. The implementation body itself won't need changes, since it merely calls super.select(...args). I don't see another route, though I could be overlooking something.
4. Introduce a way to express data dependencies
A data dependency is essentially an observable of data that shapes the state at some later point. If you can't encode that into the store itself, then the store isn't actually declarative or reactive — it just sits idle waiting for outside code to dictate its behavior through imperative calls.
In my implementations, I named this method react, since the store needs to respond when those observables emit. (RxAngular was the outlier — I handled it in the constructor. Not entirely convinced that was the right call, but it functions.)
The syntax for declaring data dependencies is consistent across every library (RxAngular aside):
this.react<ColorsStore>({
setColors: orange$,
setAllToBlack: blackout$,
});
orange$ originates from an RxJS timer, which kicks off a countdown before emitting a value — mimicking how an HTTP observable behaves. setColors is the method that consumes whatever orange$ emits and assigns a fresh state to the store. When a component re-subscribes to a selector observable via our select method, it likewise re-subscribes to each of these dependencies.
5. Lean on state adapters
A state adapter is basically an object packed with pure functions plus a selectors property that also holds pure functions. The inspiration comes from NgRx/Entity's createStateAdapter. More detail on my approach lives in the StateAdapt docs.
In my colors app implementations, I reserved state adapters for RxAngular only. The reason: RxAngular's state change functions are already pure by default, so relocating them into an adapter object required almost no effort. The state change functions in the other libraries depended on a this.setState or similar call, meaning you'd need to refactor those into pure functions before placing them in an adapter. That said, consolidating state logic in a single place still pays off for reusability.
If you go the adapter route with pure functions, you'd need to revise the react method implementation so it calls setState itself, feeding it the return value from each observable's method, rather than simply invoking the method.
In the end, I chose not to build the colors app with state adapters except for RxAngular. It strayed too far from how state changes and selectors are traditionally defined. Regardless, state logic stays easy to reuse across all of these libraries. But adopting state adapters would make migration to another state management library even smoother — pure functions are the most direct expression of a state transition.
With that groundwork laid, it's time to examine higher levels of complexity.
4. Reusable State Patterns
Every library surveyed here handles this well.
The approach involves creating a base class that contains shared logic. The critical detail is that the base class should not declare a store (in the case of Elf) or define initial state within its constructor. Instead, child classes extend the base and supply their own store or initial state. Below are working examples for each library:
5. Observable Sources
Much of the groundwork for this was already laid.
When dealing with an HTTP request, it can be supplied directly to the react method (or the super call for RxAngular), as demonstrated below:
this.react<ColorsStore>({
setColors: orange$,
setAllToBlack: blackout$,
});
Classes introduce a limitation here. When a component needs to supply data to the store, there is no direct access to the store's constructor. An imperative call becomes necessary. A typical workaround is to declare a subject alongside the store in the same file. The component then pushes data into that subject via .next(), and the subject is wired into the react method. Alternatively, if the observable is only available within the component (such as route parameters), the react method can be invoked from both the component and the store. However, this still means the store's dependencies are being established from outside its definition.
While these approaches are functional, they fall short of being truly declarative. With a class-based structure, data dependencies originating from a component cannot be part of the store's initial declaration because the store is instantiated before the component can access it. In contrast, when stores are plain objects—as is the case with StateAdapt—they can be created through function calls. This allows a service to expose a method that accepts a component-provided observable and uses it to construct a store, thereby incorporating that observable into the store's declaration from the start.
6. Multi-Store DOM Events
This scenario is equally straightforward. A subject can be exported from the same file that contains the store:
export const blackout$ = new Subject<void>();
That subject is then consumed within the store's constructor, either through the react method or the connect method (or directly in the constructor itself for RxAngular):
this.react<ColorsStore>({
// ...
setAllToBlack: blackout$,
});
Finally, it is assigned as a class property so the component can reference it directly within its template:
blackout$ = blackout$;
7. Multi-Store Selectors
Retrieving observables from a single store is straightforward, but queries that depend on state from multiple stores pose a greater challenge. As features grow in complexity, the need for such cross-store selectors will inevitably increase.
The most effective pattern available across all these libraries revolves around a form of combineLatest. The inherent problem with combineLatest lies in its behavior when multiple inputs emit synchronously. If all three input observables produce a new value in the same tick, combineLatest will fire three separate emissions. The initial two of those emissions will mix newer values with stale ones from the other inputs. The details of this "glitch" are explained further here.
A frequent workaround is to append debounceTime(0). This operator ensures that output is deferred until all inputs have synchronously emitted their latest values, thereby avoiding the emission of intermediate mixtures. The downside is that this approach still triggers Angular's change detection cycle.
RxAngular offers a more refined solution through its coalesceWith function. Its usage can be seen below, adapted from the RxAngular colors application:
allAreBlack$ = combineLatest([
this.favoriteStore.allAreBlack$,
this.dislikedStore.allAreBlack$,
this.neutralStore.allAreBlack$,
]).pipe(
coalesceWith(animationFrames()),
map(
([favoriteAllAreBlack, dislikedAllAreBlack, neutralAllAreBlack]) =>
favoriteAllAreBlack && dislikedAllAreBlack && neutralAllAreBlack
)
);
While I don't have a complete understanding of its internal mechanics, further reading is available here. It may be a worthwhile alternative to debounceTime(0) for those concerned about performance implications in other libraries. RxAngular also provides additional utility functions that can enhance overall application performance.
Conclusion
The goal was to describe five state management patterns simultaneously, which proved to be a complex task. However, the libraries are surprisingly similar in their underlying structure, and the adjustments presented here serve to highlight that similarity even further. These modifications also achieved the primary objective: reducing both the volume of code and the reliance on imperative statements. The ease with which one can now migrate between these libraries is an added benefit.
The value of declarative state management lies in its simplicity and maintainability, and it's worthwhile to adopt these "reactive store" patterns in your own applications.
If you see an opportunity for further improvement in any of these implementations, I'd be interested to hear about it.
The next and final article in this series will focus on StateAdapt. My plan is to release StateAdapt 1.0 within the next month, with the concluding article to follow shortly thereafter.
