I changed my mind. Angular needs a reactive primitive

mfp22

Mike Pearson

Posted on December 7, 2022

I changed my mind. Angular needs a reactive primitive

YouTube

Angular developers have waited 7 years for better integration with RxJS, but this doesn't seem to be happening. Instead, the Angular team wants its own reactive primitive that can't even handle asynchronous reactivity.

This made me angry at first, but after a lot of thinking and talking with other developers, I now believe that a reactive primitive could provide an overall better developer experience than just better RxJS support, even for the most diehard RxJS fans.

  1. Better promises?
  2. A split in the Angular community
  3. Rejecting vs outgrowing NgRx
  4. Angular is not reactive enough
  5. You turned them against me!
  6. Ryan Carniato
  7. Oh yeah, RxJS actually sucks at that
  8. Selectors
  9. Colocation
  10. SolidJS signal syntax is awesome
  11. What I want
  12. RxJS compatibility is still necessary
  13. Summary and conclusion

Better promises?

Most Angular developers didn't even know about RxJS until Angular made them learn it for some core Angular APIs.

At first, most of us thought observables were just a nicer version of promises, since they could do everything promises could do, but also return multiple values over time. So we thought we would just get HTTP data like http.get(...).subscribe(data => this.data = data), and we could even handle websocket data the same way, but that would be the extent of RxJS's benefits. I think the Angular team thought of observables this way too.

But the ability to represent long-lived sources of data turned out to be far more than a slight improvement over promises. Observables enable functional reactive programming (FRP), where asynchronous code is defined declaratively. This is a huge deal. FRP can entirely eliminate race conditions and inconsistent state, improve code organization, reduce page load times, simplify state management and make naming easier. My last article explains all of this.

A split in the Angular community

But FRP requires adopting a different mindset in order to take advantage of these benefits. This is known as "thinking reactively." For most Angular developers, the opportunity to explore this mindset came with their first interactions with NgRx/Store.

Rob Wormald created NgRx in 2016 as a go-to state management library for Angular by combining Redux and RxJS. This seemed natural, because Redux was very popular in the React community, and the Redux store already came with a subscribe method, just like observables. It basically already was an observable waiting to be implemented with RxJS.

NgRx sounds really cool and powerful. It's probably really easy to use. Oh wait, how do you access current state? You can't just this.store.currentState?

Nope.

And what's more, there were no plans to add this. And there weren't even plans to plan on adding it either.

This turned out to be the result of some kind of RxJS doctrine. After further exploration, we encountered bizarre advice like "don't unsubscribe", and "don't subscribe". Can you imagine people telling you to not use .then with promises? But in RxJS, we're supposed to use withLatestFrom, takeUntil and Angular's async pipe instead. And if we use the async pipe, supposedly our apps can be more performant.

This was a fork in the road for Angular developers: Do I have something to learn, or is RxJS stupid because I can't do the first thing that came to my mind?

Fork in the road meme

Rejecting vs outgrowing NgRx

Soon after NgRx introduced more RxJS into Angular apps, alternatives started popping up that appealed to developers who didn't want to learn how to think reactively, such as NGXS and Akita.

The split in the Angular community widened as other developers took RxJS and ran with it. I remember listening to a podcast where Rob Wormald (creator of NgRx and member of the Angular team) suggested that the future of Angular would be zoneless with RxJS streams stretching all the way from event sources down to the template.

This was very exciting to me. I imagined we'd eventually have precise updates fed directly into the DOM by RxJS streams, with no need for change detection at all. This would supercharge Angular performance, which was already starting to be on par with React. I hoped that this incredible performance boost would be enough enticement for more and more Angular developers to learn how to think reactively, which would then bring the other benefits of reactivity to more and more Angular codebases: No more race conditions or inconsistent state, improved code organization, reduced page load times, simpler state management and better function names.

But this future seemed a long time away, because I kept having to fight both NgRx and Angular when writing reactive code.

For example, if you had to hit 3 services in series to aggregate data for a page, you could do this with RxJS:

data1$ = this.http.get(...);

data2$ = this.data1$.pipe(switchMap(data1 => this.http.get(...));

data3$ = this.data2$.pipe(switchMap(data2 => this.http.get(...));
Enter fullscreen mode Exit fullscreen mode

The benefit of this is that each data source is declarative, so it stands on its own, agnostic to how it may be used to define other state or features. This is extremely flexible, and has all the benefits of FRP I've already mentioned twice.

But if you want to put this data in NgRx/Store, the recommended solution is not very reactive, so it doesn't come with the full benefits of FRP. I ultimately came up with a way to wrap the NgRx API (dispatch & select) inside RxJS so I could structure my code exactly the same way I could with pure RxJS, and here is how it compared to the recommended pattern with NgRx/Effects:

NgRx/Effects vs RxJS

I used this RxJS-first approach to rewrite a feature that was using effects heavily and reduced code by 25+% and the page load + render time by 90+%.

It took pro-reactivity Angular devs longer to outgrow NgRx than it took anti-reactivity devs to reject it. But we now have some RxJS-first libaries like RxAngular and StateAdapt, which are both state management libraries that enable maximum reactivity in Angular.

So, although both NgRx and Angular introduced developers to RxJS, they were both designed in ways that made full reactivity difficult. I mentioned one issue with NgRx, but Angular had a lot more.

Angular is not reactive enough

It is obvious that RxJS was sort of an afterthought in the design of Angular. It was used to represent event streams, such as component outputs. But it was also used for route parameters, which are states changing over time. But then component inputs, which are also states that change over time, were not represented as observables. RxJS in Angular is basically a really inconsistent experience.

When I hear some Angular developers complain about how cumbersome it is to use RxJS in Angular, I can actually empathize with them. But they should be blaming Angular, not RxJS. Some things that take 4 lines of code with RxJS + Angular only take a single character with RxJS + Svelte. Angular is the only major framework for which most component libraries require dialogs to be opened imperatively, which means the async pipe can't be used, requiring manual subscription management instead. Which is itself much easier in other frameworks.

I could go into a lot more detail, and I have before, but the main idea is this: Even after all the wrapper components and utilities I can create, I still can't fix Angular's core APIs.

You turned them against me!

I have waited a long time for Angular to improve its integration with RxJS. This issue, which was one of the all-time most upvoted Angular issues, was created in December 2015! The first hope it would be worked on was when the Ivy compiler was finally finished in 2019, but after another 2 years of relative silence, the issue was closed entirely! The reason? The Angular team said, "Turning inputs into Observables would more tightly couple Angular with RxJS, and we don't want to further couple with RxJS."

What???

The last comment on that issue before it was closed was by Minko Gechev, who started by reiterating the community split over RxJS: "As we've discussed in the past, the community is very split when it comes to using less or more RxJS with Angular." At the end of the comment he said, "We will share our plans for more ergonomic RxJS APIs when we prioritize that project." For someone who had already waited for 5 years for better Angular APIs, this was very frustrating, as he gave no timeframe at all. Not only that, but the primary reason given was that many Angular developers dislike RxJS, and I strongly believe that the primary reason for this is Angular's obnoxious integration with RxJS.

RxJS turned developers against Angular meme

Then I saw that a member of the Angular team was exploring a different reactive primitive altogether. If Angular developers don't like reactivity, why would they prefer another reactive primitive over RxJS? If they don't like Angular's integration with RxJS, how would using a different reactive primitive help at all? None of it made sense to me.

Ryan Carniato

Another thing that has been bothering me has been Ryan Carniato's insistence that RxJS isn't fine-grained reactivity. I know that RxJS can be used to update the DOM in fine-grained ways, and it turns out Ryan knows it too, because he built a previous version of SolidJS with RxJS. I've been pestering him in the comment section of his YouTube streams for a few weeks now, and he finally gave me an explanation that caused something inside my head to click.

Ryan told me that the reason he calls RxJS "course-grained" reactivity is because, while you technically can perform fine-grained updates with it, people tend to combineLatest with streams instead. Is it fair to say that RxJS isn't fine-grained because people tend not to use it in fine-grained ways? I don't think so, but Ryan's main point was actually interesting. He caused me to make a mental connection that I had never made before.

Oh yeah, RxJS actually sucks at that

When I first used NgRx, I wanted to use RxJS for everything, so I relied heavily on the map operator for derived state. But nowadays, derived state is completely implemented with selectors in pretty much all NgRx projects. So, whereas modern NgRx will combine state from multiple reducers like this:

const selectItems = createSelector(state => state.items);
const selectFilters = createSelector(state => state.filters);

const selectFilteredItems = createSelector(
  selectItems,
  selectFilters,
  (items, filters) => items.filter(filters),
);
Enter fullscreen mode Exit fullscreen mode

Originally I did it like this:

items$ = this.store.select(state => state.items);
filters$ = this.store.select(state => state.filters);

filteredItems$ = combineLatest(
  this.items$,
  this.filters$,
  ([items, filters]) => items.filter(filters),
);
Enter fullscreen mode Exit fullscreen mode

I was pretty happy with this RxJS approach, until my team lead put a console log in the filter function and saw that it was being run 28 times and asked me why. I basically told him, "I have no idea, I swear FPR makes apps more performant."

My first idea was to use distinctUntilChanged. That reduced some of the reruns.

Then I realized that every single observable that chained off of filteredItems$ caused the filter function to run again. The solution? shareReplay(), except actually publishReplay(), refCount() because of some weird RxJS issue. That also reduced some of the reruns.

Then I realized that whenever both items and filters were changed at the same time, the combineLatest would run twice, once per input observable. This was inefficient, but also caused errors in some states, because it was annoying to handle the intermediate situations where one input had an updated value but the other didn't.

I really wanted to get RxJS to work with this. I was only a junior developer at the time, but I spent hours and hours trying to think a way around this problem with RxJS, and came to the conclusion that it had to involve schedulers, but I didn't understand how to actually make it work.

At that point, I began to question my goal. Look at all the work I've had to go through, and it still feels awkward. Do we really want to create custom operators to make nice syntax for all of this? Why doesn't RxJS make handling derived states easier? Would it be so bad just to use selectors?

There were a lot of other Angular developers struggling with this stuff at the same time I was, and all of us concluded that selectors were the way to handle derived state in NgRx.

Selectors

Every time I see a new state management library pop up, I check to see if they use selectors, or if they require you to use distinctUntilChanged, publishReplay, refCount, combineLatest and debounceTime. Most of them do, or you have to be okay with inefficient code. RxAngular is an exception with not requiring the debounceTime, since it gives you coalesceWith, which is awesome.

I never liked the syntax of selectors, but they seemed necessary to me. So when I designed my own state management library, StateAdapt, I wanted to find a better syntax for selectors. This is what I came up with:

// NgRx:
const selectItems = createSelector(state => state.items);
const selectFilters = createSelector(state => state.filters);

const selectFilteredItems = createSelector(
  selectItems,
  selectFilters,
  (items, filters) => items.filter(filters),
);

// StateAdapt:
const adapter = buildAdapter<State>()({})({
  items: s => s.state.items,
  filters: s => s.state.filters,
})({
  filteredItems: s => s.items.filter(s.filters),
})();
Enter fullscreen mode Exit fullscreen mode

I used a proxy object s (stands for both state and selectors) that watches for the selector you're accessing and creates a more efficient memoization than createSelector.

I've been very happy with this approach. I have thought that nothing more was needed in Angular than RxJS for events and async logic, and selectors for synchronous, derived state.

But it turns out that selectors aren't perfect either.

Colocation

There are 3 types of selectors.

State selectors

These are selectors that simply select from a state object:

const selectItems = createSelector(state => state.items);
Enter fullscreen mode Exit fullscreen mode

It makes sense to export these with the reducers that manage the state they're selecting from.

Derived selectors

These are selectors that combine state from multiple reducers/stores and calculate derived state. This is derived state that doesn't belong with the top-level state, and these selectors can contain a lot of business logic.

UI selectors

These take the derived state from the first 2 selectors and change the shape of the data to be convenient for consumption in a template.

For example, let's say I had some state that described a rectangle with a position like { x: 50, y: 200 }. If that needs to be rendered as a div with CSS like style="left: 50px; top: 200px" then we could have a UI selector like this:

itemWithStyle: s => ({ left: `${s.x}px`, top: `${s.y}px` }),

<div [ngStyle]="store.itemWithStyle$ | async"></div>
Enter fullscreen mode Exit fullscreen mode

Then the component can stay lean and you can test this selector as a pure function by directly importing the state adapter and passing values to it. That's really nice. And where is a better place to put a pure function than in a selectors file where it can be tested as a pure function instead of as part of a component? And as part of a state adapter, it's reusable by default, whereas if you put the logic in the component template, you'd have to extract it in order to reuse it.

But after trying out putting UI selectors inside state adapters for a while now, I have decided that managing the same concern across 2 separate files was too awkward for the small benefit of having it in an adapter. It felt like I was polluting the derived selectors file with really boring UI concerns, and those UI concerns were more convenient colocated with the template anyway. Imagine if the template needed to implement this as an SVG. Wouldn't it be convenient to be able to edit this logic in the template instead of in a separate file? Also, in what situation would you want to reuse this UI logic? Probably when you want to reuse the component too, right?

But how do we define efficient derived state in the component? In StateAdapt it's actually not that hard, but it's not super easy either, and it definitely isn't convenient with the NgRx-style syntax.

Component inputs and the diamond problem

Another issue with both RxJS and selectors is the question of how to deal with component inputs.

For a long time, I just wanted component inputs as observables. But it turns out there's a small issue with this: If each component input gets a new value at the same time, each input observable would fire in succession, so if you needed to combine input values inside the component, a combineLatest would fire once for each input observable. Selectors can't help with this, since component inputs aren't part of the global store.

This problem actually has a name: The diamond problem.

But you know what does have really awesome syntax and handles the diamond problem perfectly? A reactive primitive that isn't RxJS: Signals.

SolidJS signal syntax is awesome

Here's a simple SolidJS component:

const CountingComponent = () => {
    const [count, setCount] = createSignal(0);
    const doubleCount = createMemo(() => count() * 2);
    return (
        <div onClick={() => setCount(count() + 1)}>
            Double count value is {doubleCount()}
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

Here's the same thing with Angular and RxJS:

@Component({
  selector: 'app-counter',
  template: `
    <div (click)="count$.next(count$.value + 1)">
      Double count value is {{doubleCount$ | async}}
    </div>
  `,
})
export class CounterComponent {
  count$ = new BehaviorSubject(0);
  doubleCount$ = this.count$.pipe(
    map(count => count * 2),
    distinctUntilChanged(),
    publishReplay(),
    refCount(),
  );
}
Enter fullscreen mode Exit fullscreen mode

The SolidJS syntax is better for many reasons:

1. Derived or not?

In Angular you need to know you're going to have derived state in order to have a reason to use BehaviorSubject right off the bat. With SolidJS, all of your state that changes will be signals, and whether they eventually get used to derive other state does not matter. So you never need to rewrite a signal with different syntax after you first create it.

2. Recomputing

By default, SolidJS's signals will not recompute if the state they're derived from doesn't change. No need for distinctUntilChanged.

3. Shared

No matter how many signals are derived from a signal, it will not recalculate for each of them, unlike RxJS which runs a map function for each derived observable, unless you use publishReplay(), refCount().

4. Combining

When combining SolidJS signals, you don't need to define the dependency array up front like you would with NgRx selectors or combineLatest. You can just define it like c = createMemo(() => a() + b()).

And the issue with RxJS's combineLatest running once for each input is not a problem with signals. SolidJS signals wait for each dependency to finish running before they run. SolidJS signals elegantly handle the diamond problem.


Okay, now let's compare SolidJS with some examples using selectors. Here's the SolidJS code again:

const CountingComponent = () => {
    const [count, setCount] = createSignal(0);
    const doubleCount = createMemo(() => count() * 2);
    return (
        <div onClick={() => setCount(count() + 1)}>
            Double count value is {doubleCount()}
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

Let's try Angular with StateAdapt:

@Component({
  selector: 'app-counter',
  template: `
    <div (click)="count.increment()">
      Double count value is {{doubleCount$ | async}}
    </div>
  `,
})
export class CounterComponent {
  count = adapt(['count', 0], {
    increment: state => state + 1,
    selectors: {
      double: state => state * 2,
    },
  });
}
Enter fullscreen mode Exit fullscreen mode

That's actually not bad! But it's a little more code than it has to be, as you'll see below.

Angular with NgRx would be too long, but you can imagine defining the selector in the component file:

@Component({
  selector: 'app-counter',
  template: `
    <div (click)="increment()">
      Double count value is {{doubleCount$ | async}}
    </div>
  `,
})
export class CounterComponent {
  selectDoubleCount = createSelector(
    selectCount,
    count => count * 2,
  );
  doubleCount$ = this.store.select(this.selectDoubleCount);

  constructor(private store: Store) {}
  //...
}
Enter fullscreen mode Exit fullscreen mode

Basically, selectors get the job done efficiently, but I've never seen a way to use them with as little code as signals require.

Wouldn't it be nice to have a reactive primitive in Angular that's efficient by default, with minimal syntax?

What I want

How about this:

@Component({
  selector: 'app-counter',
  template: `
    <div (click)="count.set(count.get() + 1)">
      Double count value is {{doubleCount.get()}}
    </div>
  `,
})
export class CounterComponent {
  count = signal(0);
  doubleCount = memo(() => this.count.get() * 2);
}
Enter fullscreen mode Exit fullscreen mode

SolidJS's tuple syntax isn't available, since we're assigning class properties here, but this is pretty good too.

And of course there should be a way to specify Angular inputs as signals:

@InputSignal: count!: Signal<number>;
Enter fullscreen mode Exit fullscreen mode

RxJS compatibility is still necessary

Signals are great for synchronizing derived state, but RxJS is still necessary for asynchronous reactivity. If you have any nontrivial amount of experience using RxJS it should be obvious why, but I explain a lot more in this article.

So, it would be cool if the Angular team found a way to make converting from signals to observables extremely convenient:

export class CounterComponent {
  count = signal(0);
  doubleCount = memo(() => this.count.get() * 2);
  delayedCount$ = this.count.pipe(delay(1000));
}
Enter fullscreen mode Exit fullscreen mode

The pipe would convert the signal to an observable and pass the arguments to the new observable's pipe method.

Since we're accessing an observable, hiding that pipe() logic behind a lazy import could be possible, and that would allow the RxJS code to not be loaded until it was needed.

Or Angular could copy how SolidJS does it:

const CountingComponent = () => {
    const [count, setCount] = createSignal(0);

    // signal to observable (`from` imported from 'rxjs'):
    const count$ = from(observable(count)); 

    // observable to signal (`from` imported from 'solid-js'):
    const countAgain = from(count$);
};
Enter fullscreen mode Exit fullscreen mode

Summary and conclusion

I have some strong opinions, but I also seem to change my mind a lot. But it's not like I just love moving from each JavaScript fad to the next every month. It has taken a lot of experience and pain to form my opinions, and everything I learn adds onto them.

First I thought RxJS was just a slight improvement over promises.

Then I learned about the benefits of thinking reactively, and wanted to use RxJS everywhere.

Then I learned that synchronizing state was better done with memoized selectors than with RxJS. But for asynchronous logic, RxJS was amazing, and I wanted better APIs than what Angular and NgRx provided out of the box.

Then I encountered Ryan Carniato's version of fine-grained synchronous reactivity, as well as his streams about colocation in Marko 6, and lastly people's comments on the old Angular issue describing the diamond problem for component inputs. All of this was swimming around my head until my conversation with Ryan about RxJS's problems. That's when it all fell into place and I realized there was an important role for a reactive primitive that wasn't RxJS or selectors.

So,

  • RxJS is necessary for asynchronous reactivity.
  • Selectors are necessary for reusing state management patterns with state adapters.
  • A simple reactive primitive is necessary for simple, local, reactive state synchronization.

As I work to improve StateAdapt, I will need to figure out how the new reactive primitive fits into the state management picture. I will probably start by exploring how it can be used most easily with SolidJS.

We all have different pieces of the puzzle, and I have faith that the web development community will eventually find beautiful, declarative syntax and ways of efficiently training developers with the right mindset to take advantage of reactive programming, and our industry will become increasingly productive and enjoyable to work in.


Thanks for reading!

I'd love to hear your own personal experience with anything I've shared in this article.


Cover photo by Casia Charlie: https://www.pexels.com/photo/sea-dawn-landscape-nature-2433467/

💖 💪 🙅 🚩
mfp22
Mike Pearson

Posted on December 7, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related