DEV Community

Cover image for Progressive Reactivity with NgRx/Component-Store, Akita, Elf, RxAngular and RxJS
Mike Pearson for This is Angular

Posted on • Edited on

Progressive Reactivity with NgRx/Component-Store, Akita, Elf, RxAngular and RxJS

In this series I came up with 3 rules to achieve progressive reactivity. Following them reduced the code for NgRx/Component-Store by 12%, Akita by 9%, Elf by 13%, RxAngular by 7%, and RxJS ("Subjects in a Service") by 22%. Here they are again:

  1. Keep code declarative by introducing reactivity instead of imperative code
  2. Don't write callback functions
  3. Wrap imperative APIs with declarative ones

Let's walk through each level of complexity and see how reactivity reduced the code, making the syntax more progressive as well.

Level 3: Complex Changes and Derived State

All of these libraries rely heavily on RxJS. From Part 3 of this series:

Now that we're using RxJS, we need to keep in mind that RxJS is powerful. It can handle anything, even things that maybe it shouldn't. If we aren't careful, our innocent-looking RxJS pipes will grow and grow until they become a sprawling monster that makes our teammates want to trash RxJS on Twitter.

There isn't a definite line between "should use RxJS" and "this is too complex for RxJS." But these are the signs you're at Level 3 complexity:

  • You use a tap operator with console.log to debug a stream. You need devtools.
  • You use distinctUntilChanged, share or combineLatest or other merge operators. You need memoized selectors.
  • You wish you could spread objects and arrays in templates to pass to behaviorSubject$.next(...), or you are tempted to create methods to imperatively make those changes from afar. You need a centralized collection of ways your complex object/array can change. RxJS can do this with scan(), but it's a lot of boilerplate.

So we want:

  • Devtools
  • Selectors
  • Centralized, declarative state changes

This is starting to sound like a state management library.

Level 3 complexity is just the start of the need for these things. From here on, the more your features grow, the more unwieldy the RxJS will become.

Redux Devtools provides an extremely valuable window into the state of your application. But out of these libraries, only Akita and Elf support it, and only in a limited way.

None of these libraries have selectors, but they do provide utilities that make the RxJS more bearable. RxAngular is especially good. (For RxJS "Subjects in a Service" I am assuming you have created a base class like this that adds a distinctUntilChanged at the end of a select method.)

How about centralized, declarative state changes?

Well, out of the box, only RxAngular provides a way to define centralized, declarative state changes. When I was trying it out, I loved how similar to StateAdapt it felt. The only issue I noticed was that it creates subscriptions that either last as long as the component you're using the state in, or they last forever if it's used in a service. This means if you use it in a service to share state among 2+ components, when you navigate away and back to it, HTTP requests will not be re-triggered without calling something from the component, which resulted in RxAngular having 5 imperative statements instead of 4.

Still, RxAngular was amazing compared to the other libraries. The rest are heavily imperative, each requiring 10 total imperative statements in the most complex example I implemented. I wouldn't blame you if you just stuck to vanilla RxAngular, but the other libraries could definitely use some reactive utilities.

It's good these libraries exist for features that are very simple. But features aren't static, and when they grow to more complexity, we end up regretting using something that wasn't well-suited for what the feature has become.

But we still need to deal with it. The reactive utilities I'm about to show you help with this. First, they make your code less imperative, which means more comprehensible and maintainable. But they also cause the implementations for each library to look strikingly similar to StateAdapt, and even the reactive styles of NgRx/Store and NGXS which I explained in the last article, so you can more easily migrate complex features to a library that supports selectors and Redux Devtools.

So, here are the 5 suggestions that will form the foundation for the rest of the article.

1. Use services for state

Some of these libraries give you the option to have your component class extend a base class they provide. I suggest not doing this. The reason is because you may need to move it to a service later, and this will create unnecessary work because the way you interface with state will change once it's encapsulated in a service, so you will need to update a lot of places in your template. It's better to just start with a service and avoid that minor syntactic dead end.

2. Extend your own base class

Rather than extending the class they give you directly, I would define your own class to extend for all your state services (for Elf, just create a class). This is where we will insert our reactive middleware. I just called the class ReactiveStore for all of them except Akita, for which I named it ReactiveQuery. Here are the links to my implementations:

3. Override (or create) the select method

I wish this wasn't necessary, but it definitely is. It's because when our state is in a service, it's going to live forever. That means we need a way to subscribe/unsubscribe in a way that causes HTTP requests to automatically fire again when navigating back to a component that uses the state. I'll explain part of the implementation in the next suggestion. But you can see my full implementations for each library at the links in the previous suggestion.

Overriding select means for RxAngular, Akita and NgRx/Component-Store you may occasionally have to update the TypeScript method overloads to match any updates from the libraries. Just click to the definition and copy/paste their overload types above your select implementation, importing whatever other types you need from their library. Nothing with the actual implementation will need to ever change, since we're just calling super.select(...args). I don't think there is another way to do this, but I could be wrong.

4. Create a way to declare data dependencies

A data dependency is simply an observable of data that will influence the state at some point. If you can't declare this as part of a store, then the store is not declarative/reactive; it merely sits there waiting for other code to control it with imperative commands.

In my implementations, I called this method react, because the store needs to react to the observables when they emit values. (RxAngular was an exception. I just used the constructor for it. Not convinced it was the best decision, but it works.)

The syntax for each library (other than RxAngular) for declaring data dependencies is identical:

  this.react<ColorsStore>({
    setColors: orange$,
    setAllToBlack: blackout$,
  });
Enter fullscreen mode Exit fullscreen mode

orange$ comes from an RxJS timer, which causes it to start counting down before emitting a new value, mimicking an HTTP observable. setColors is the method that will take the value emitted by orange$ and set a new state for the store. When a component re-subscribes to a selector observable from our select method, it will also re-subscribe to each of these observables.

5. Use state adapters

A state adapter is an object full of pure functions and a property called selectors, which is also full of pure functions. They are inspired by NgRx/Entity's createStateAdapter. You can read more about how I do them in the StateAdapt docs.

In my implementations for the colors app, I only used a state adapter for RxAngular. The reason was that by default the state change functions in RxAngular are already pure functions, so dragging them over to an adapter object was extremely easy. The other state change functions for the other libraries involved calling a this.setState or equivalent function, so I (and you) would have to refactor those to be pure functions before moving them to an adapter. Still, having all the state logic in one place helps with reusability anyway.

If you do decide to create an adapter with pure functions, you would need to change the implementation of the react method to call setState itself, using the return value from the method for each observable, instead of just calling the method itself.

In the end, I decided not to implement the colors app using state adapters for any library except for RxAngular. It was too much of a departure from how state changes and selectors would normally be defined. Either way, state logic is easy to reuse in all of these libraries. But if you used state adapters, it would be even easier to migrate to another state management library. Pure functions are the simplest way to express a state change.


Alright, with that foundation in place, let's look at higher levels of complexity.

4. Reusable State Patterns

This is easy with all of these libraries!

All you have to do is extend a base class. In the base class, don't define a store (Elf) or initial state in a constructor at all. Just extend that base class and define the store or initial state in the child classes. Here are my implementations for each:

5. Observable Sources

We already did the hard work for this.

If you have an HTTP request, just pass it into the react method (or super call for RxAngular) like this:

  this.react<ColorsStore>({
    setColors: orange$,
    setAllToBlack: blackout$,
  });
Enter fullscreen mode Exit fullscreen mode

Classes have one disadvantage. If you need to pass something from the component to the store, you will have no access to the constructor, so you need to make some imperative call. I would define a subject in the same file as the store and pass the data from the component to it with a .next(), then that subject can be passed into react. If it's actually an observable only the component has access to (such as route params), you can call the react method both in the component and in the store itself. However, both of these are defining the store's dependencies from the outside.

This is a good solution, but it is not ideal, because it isn't declarative. With classes, data dependencies from a component cannot be included in the declaration of the store, because the store is already defined and instantiated by the time the component gets a hold of it. If stores are just objects, as they are in StateAdapt, you have the flexibility to create stores with a function call, so in a service you can have a method that takes in an observable from a component and creates a store with that observable. This allows the observable to be part of the store's declaration.

6. Multi-Store DOM Events

This is easy too. You can export a subject from the same file as the store:

export const blackout$ = new Subject<void>();
Enter fullscreen mode Exit fullscreen mode

Use that in the react method in the store's constructor (or connect or constructor itself for RxAngular):

  this.react<ColorsStore>({
    // ...
    setAllToBlack: blackout$,
  });
Enter fullscreen mode Exit fullscreen mode

And then assign it as a class property so the component can use it directly from the template:

blackout$ = blackout$;
Enter fullscreen mode Exit fullscreen mode

7. Multi-Store Selectors

We know how to get observables from each store, but what if we need state from multiple stores somewhere? The more complex your features become, the more of these kinds of selectors you'll wish you could have.

The best we can do for all of these libraries is a kind of combineLatest. The issue with combineLatest is that every input emits one-by-one, so if all 3 input observables have a new value at the same time, combineLatest itself will output 3 times, and the first 2 times will be returning some previous values mixed with current values. You can read more about the problem here.

A common solution is to use debounceTime(0), because it waits for all synchronously updated inputs to have new values before emitting. However, this will trigger change detection.

RxAngular has a cool function called coalesceWith. You would use it like this (example taken from the colors app for RxAngular):

  allAreBlack$ = combineLatest([
    this.favoriteStore.allAreBlack$,
    this.dislikedStore.allAreBlack$,
    this.neutralStore.allAreBlack$,
  ]).pipe(
    coalesceWith(animationFrames()),
    map(
      ([favoriteAllAreBlack, dislikedAllAreBlack, neutralAllAreBlack]) =>
        favoriteAllAreBlack && dislikedAllAreBlack && neutralAllAreBlack
    )
  );
Enter fullscreen mode Exit fullscreen mode

I don't totally understand it, but you can read more about it here. It might be worth using with other libraries too, if you're concerned about performance problems with debounceTime(0). RxAngular has other tools that would improve performance too.

Conclusion

Hopefully that helps. It was a little complicated describing 5 state management patterns at the same time, but they are surprisingly similar, and these changes make them even more similar. They also reduced lines of code and imperative statements, which was the main goal. The fact that it's more and more trivial to migrate between them is just a bonus.

I hope you see the value in the simplicity and maintainability of managing state declaratively, and take steps to use these "reactive stores" in your projects.

Let me know if you see an opportunity to improve any of these.


Next will be the 12th and final article in this series. It will be about StateAdapt. I hope to release StateAdapt 1.0 within a month, and publish the 12th article shortly after.

Top comments (0)