DEV Community

Cover image for I changed my mind. Angular needs a reactive primitive
Mike Pearson for This is Angular

Posted on • Updated on

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/

Top comments (63)

Collapse
 
mtmtmt profile image
MT • Edited

The question is, why not just implement signals with RxJS, that was promoted for almost a decade?
It's a BehaviorSubject + shareReplay + distinctUntilChanged + (perhaps) knowledge about the component lifecycle.
Why not just create something like EventEmitter? EventEmitter extends Subject, and Signal (the name may differ) could extend BehavriorSubject with ANY additional functionality needed, specific to Angular:

value$ = new Signal<...>(...); // Signal extends BehaviorSubject just like EventEmitter extends Subject.
Enter fullscreen mode Exit fullscreen mode

Or with decorators:

@Signal(...) value$;
Enter fullscreen mode Exit fullscreen mode

It may know about component's ngOnDestroy etc. and be aware about Angular's inner workings.

I've read your article, I understand that you're convinced, but to be honest I still see the main thing: Angular failed in integration with RxJS. And not they think that adding one more concept gonna change anything. No it won't.

Why not adopt rx-angular, improve RxJS debugging with e.g. Angular DevTools, build more creational operators, improve error handling, and most importantly: improve the communication with the community?

Let's face it: core of Angular team has left, and now the "new gen" Angular Team just uses it as their lab for experiments, following "trends" and trying to "sell" it. The future of the framework is vague. They have dropped improving the best parts of it, rewriting the engine every 2 years and changing their mind.

By the way, they officially fake HMR: the app is fully re-bootstrapped each time: github.com/angular/angular/issues/... And it's 2023.

So now they're gonna add "signals", then "hooks", then they would rewrite the Engine again "to support the brand new signals thing", then they'll drop RxJS "because signals are better", etc.
While they could improve RxJS and Angular's integration with it instead.

Now the question for you, Mike. Are you really really sure that RxJS can't cover what signals do? Maybe with another creational operator. Maybe with some improvements into RxJS itself as a lib. Are you really convinced or just gave up? :)

Collapse
 
mfp22 profile image
Mike Pearson

I've tried so hard. But it's such a hard thing to do to convince developers of the simplicity RxJS can bring when they haven't experienced a truly complex codebase, and ultimately the Angular team is downstream from the community. Something like this was inevitable.

But I've also been listening to Ryan Carniato's streams and I'm convinced there's just no way RxJS could be made to be as performant as signals for state synchronization. Syntax matters more to me personally. But performance is awesome too.

As for complexity, I think most people will be surprised at how much simpler this makes everything. It's a new concept, but we get to drop a few concepts too: No more Async pipe, no more *ngIf tricks, easy access to observable values in TS without subscribing... Probably more.

One thing I am concerned about is that they'll go too far and completely rip RxJS out even from places that work really well with RxJS. When your design decisions are driven by public opinion, you can only go so long before you create an inconsistent mess with breaking changes every year when everybody experiences pain that could have been foreseen and demands the knee-jerk reactionary opposite. If we move away from RxJS, the pain will be more and more spaghetti code.

Collapse
 
mtmtmt profile image
MT • Edited

And what about contributing in RxJS to add the signal as another entity there? Make it better, make the learning curve smoother, whatever.

import { signal, of, fromSignal } from 'rxjs';
import { toSignal } from 'rxjs/operators';

const observable: Observable = of([]);
const signal: Signal = signal(1);

const signal2 = observable.pipe(map(...), flatMap(...), toSignal());
const observable2 = fromSignal(signal2);

const signal3 = () => signal2() * 2;
Enter fullscreen mode Exit fullscreen mode

They already have e.g. Subjects, why not add Signals too...

Collapse
 
tmish profile image
Timur Mishagin

Yes, can agree with you. For me it's more and more clear that sooner or later it will be easier to migrate a project to Svelte rather than trying to get in touch with Angular's "cutting edge" point of view.

Why do I need to bother in choosing between RxJs or signals if I can just use Svelte's stores? It's just stupidly easier, isn't it?

Collapse
 
mfp22 profile image
Mike Pearson

Ugh, Svelte stores are the worst. They're just less-capable versions of BehaviorSubject.

Thread Thread
 
tmish profile image
Timur Mishagin • Edited

Maybe, but I don't have any example which might be a proof that BehaviorSubject is superior...

By the way, signals are not bad, but the way it's implemented in Angular it's bsht as for me. If Angular wants to catch up the others why should I go with it if others do it better (less verbose, more elegant and so on)?

I just looked at how ngrx is adopting signals and I was about to bloat. I wanted to say there the same crytics as here, but I didn't. I don't want upset the developers since they're at least trying. As it was mentioned it's not NgRx or RxJs, but Angular...

Thread Thread
 
mfp22 profile image
Mike Pearson

Anything with .pipe() is an example where BehaviorSubject is superior.

If you watch the video with Ryan Carniato and understand how the Angular team made signals, I actually think it's the best designed they could have possibly come up with, given the constraint that they are to be used in classes. The interop with RxJS could be better, but the signals themselves are very, very good.

Collapse
 
mpolutta profile image
Mat Polutta

And now I get it. I transitioned from AngularJs to Angular years back. I then transitioned from Promises to RxJS. Later I was assigned to a Dynamics CRM team where I created an Angular Library to cover all of the custom and missing components for our CRM apps. Well, whenever I went back to RxJS Observables for bug fixes or enhancements, it made my head hurt. Your examples make it very clear why. I am currently converting the Angular library to the Dynamics 365 PCF (which uses React). The Dynamics API provides for Promises, and that is what I first ported. Very timely, because my next component is invoking an LDAP API which I wrapped with RxJS Observables. It seems I'll keep it that way.

Collapse
 
omergronich profile image
Omer Gronich

SolidJS signals wait for each dependency to finish running before they run.

Not sure this is true TBH. Just tested this in the solid playground and it seems solid memos behave the same way as combineLatest here.

try running this solid code and check the console:

import { render } from "solid-js/web";
import { createMemo, createSignal } from "solid-js";

function Counter() {
  const [count, setCount] = createSignal(0);
  const [count2, setCount2] = createSignal(1);
  const increment = () => {
    setCount(count() + 1);
    setCount2(count2() + 1);
  };
  const derived = createMemo(() => {
    console.count('run calculation');
    return count() + count2();
  })

  return (
    <button type="button" onClick={increment}>
      {count()} 
      <br / >
      {count2()}
      <br />
      {derived()}
    </button>
  );
}

render(() => <Counter />, document.getElementById("app")!);
Enter fullscreen mode Exit fullscreen mode

The memo runs twice even though the signals were changed "at the same time". It can be solved using batching or scheduling but that's true for RxJs also.

The quotes are because values in JavaScript rarely ever change at the same time, I think multiple component inputs in angular can't ever change at the same time because angular always creates them in order synchronously.

Great article regardless, I enjoyed it thoroughly :)

Collapse
 
mfp22 profile image
Mike Pearson

Glad you liked it!

So what I mean by "same time" is literally same time, not just synchronous. It's called the diamond problem because you'll have a state at the top, then 2 derived states from that one, then a final derived state that combines them back again. So it's shaped like a diamond. So more like this:

import { render } from "solid-js/web";
import { createMemo, createSignal } from "solid-js";

function Counter() {  
  const [count, setCount] = createSignal(0);

  const derived1 = createMemo(() => count() * 10);
  const derived2 = createMemo(() => count() * 100);

  const combined = createMemo(() => {
    console.count('run calculation');
    return derived1() + derived2();
  })

  return (
    <button type="button" onClick={() => setCount(count() + 1)}>
      {count()} 
      <br / >
      {derived1()}
      <br />
      {derived2()}
      <br />
      {combined()}
    </button>
  );
}

render(() => <Counter />, document.getElementById("app")!);
Enter fullscreen mode Exit fullscreen mode

It's actually expected if you were to set the signal values sequentially that they would run twice like in your example. In reactive programming, you never update more than one thing for each event. (Technically you shouldn't update anything, and there should just be streams that listen to events, but whatever.) You probably know, but in React it would queue the updates and rerender only once. This actually makes imperative programming easy in React. That's something I'm not interested in.

Thanks for the comment. I didn't actually verify this before now; I just assumed based on things Ryan Carniato has said that signals behaved the optimal way. And I just checked it and it is right:

SolidJS Diamond

The diamond problem actually does come up quite often in real world apps. Not all the time, but often.

Collapse
 
omergronich profile image
Omer Gronich

Ohhh gotcha. That's cool. Thanks for the reply!

Collapse
 
amitbeck profile image
Amit Beckenstein • Edited

Very interesting read. There's indeed lots of issues with Angular and RxJS and I do hope that all the reworks of Angular prove to be good solutions. Making NgModules optional was already a great step towards better DX.

Collapse
 
mfp22 profile image
Mike Pearson

Definitely

Collapse
 
tronicboy1 profile image
井上 オースティン

I really enjoyed your article, thank you!

I think Angular’s decisions to lower the initial learning curve is going to lead to more interest in the long term and Signals could be a big part in helping that.

The biggest reason why I use angular is that it can solve code organization problems that other libraries frankly are ill equipped to solve. The injection systems, Services, Angulars Router, Module system, these are all very well done and can be used to organize big code bases.

I am worried like others that angular may drift from RxJS as RxJS is very very much a high end master level library.

Interoperability between signals and Observables as seen in SolidJs could give us the best of both worlds, removing the clutter of the Async pipe.

The angular team is saying that they are prioritizing interoperability so this could be a way to fix the problems of RxJS integration.

Id love to see as @InputSignal decorator which could also be converters to an Observable then piped.

Collapse
 
antischematic profile image
Michael Muscat

Signals make sense for functional components. I'm not convinced that signals alone will make Angular class-based components better.

I hope that whatever solution Angular comes up with works as well in TypeScript code as it does in templates, has interop with RxJS and handles things like inputs, host bindings and queries seamlessly, and isn't an eyesore to look at.

Collapse
 
mfp22 profile image
Mike Pearson

I think it will, as long as it doesn't try to mimic something from somewhere else beyond what makes sense for Angular. If these basic requirements are met, I'm sure people will like it.

I prefer function components, but besides syntax, it doesn't make a big difference

Collapse
 
sebosek profile image
Sebastian

Partially agree with @oz, about negative under tone.
However, I think it creates nice contrast to proposed bright future.
And your proposed API? Love it! ❤️
Short, simple, keen, easy to use, located to scope of the component. It would play soooo nicely with Standalone components.
You know what? You've restored my faith in Angular 😄

Collapse
 
mfp22 profile image
Mike Pearson

I'm completely clueless about the implementation details, so there might be unexpected limitations from that. And I thought for about 5 minutes before coming up with this, so there's a good chance there's something better. But I like concise things.

As for the negativity, on certain topics I can't help it. This is my experience and I don't see a point in dressing it up. I couldn't decide what audience to write for, because there's something for every perspective to hate and love in this. So I just included all the truth I thought was relevant.

Collapse
 
maslow profile image
Maslow

Very interesting read

Collapse
 
martinmcwhorter profile image
Martin McWhorter

Yes. A thousand times yes.

Collapse
 
jason_aunkst_9623286aac3b profile image
Jason Aunkst • Edited

RxJS is still more portable than an angular signal primitive.

Also you’re not going to deal with async situations like switch map with a signal.

Collapse
 
mfp22 profile image
Mike Pearson

Yes. I just think of signals as a good way of managing component-level reactivity. Like AsyncPipe++

Collapse
 
alexandis profile image
alexandis

Oh goodness... It's still too much for my brain. I got rid of state in my code. And I do use behaviorsubject and combinelatest. It does work. I know it's not optimal, but all these things and approaches stuffed into Angular apps... It's just too much to get hands into all this. It needs to be simpler.

Collapse
 
mfp22 profile image
Mike Pearson

You will like signals

Collapse
 
alexandis profile image
alexandis

I already use SignalR. But I still use BehaviorSubject and CombineLatest as well... :-D

Collapse
 
bretto profile image
Brett

Did you check ? rx-angular.io/

Collapse
 
timsar2 profile image
timsar2

‌Yes he did, He preferred rx-angular until stateAdapt being stable.

Collapse
 
jwp profile image
John Peters

To me Angular's glory days ended about 7 years ago. The core team left after Angular 2, and it took them all that time to address the Ngmodel clash in Angular 14 stand alone components. With Svelte we see simplicity where even React is heavy weight.

Collapse
 
mfp22 profile image
Mike Pearson

There are a lot of good people working on Angular. I love Svelte, but a lot of web apps depend on Angular and it's a lot better for them to have an improved Angular than to migrate to something else. Angular is on par with React in every way except syntax, and signals will bridge that gap a bit and hopefully enable a performance boost to something literally impossible with React, and faster than current Svelte. It would take a big effort but I think it's possible.

Collapse
 
intermundos profile image
intermundos

Another great article of why not to use angular. Over engineering at its best.

Collapse
 
mfp22 profile image
Mike Pearson

Glad you liked it!

Collapse
 
timsar2 profile image
timsar2 • Edited

I would like to start a new project and use state-adapt, but this two week ppl are talking about signals and I think it would be good to use signal now instead of refactoring my app 6 month later.
It would be good if you can add a sample implemented with signals to state-adapt, like ngrx/signal-store:
github.com/ngrx/platform/discussio...

Collapse
 
mfp22 profile image
Mike Pearson • Edited

Here's what will not change with signals:

  • Sources
  • Selectors & state adapters

Here's what will change:

  • Not using the async pipe
  • Might add a convenience function, but seems a bit unlikely for now

StateAdapt takes advantage of RxJS behavior of doing nothing until something needs it. fromObservable subscribes immediately. This means the role for signals is pretty small in StateAdapt.

Signals are good for synchronously derived, non-reusable state. So, state that is forever local to a component. That's a very small part of overall state management.

All these libraries diving headfirst into signals are going down a path that restricts how reactive and flexible state management can be. It's not a good idea. Especially this early on... The best way to reduce the amount of refactoring you'll need to do is to start very conservatively with signals and learn all about them before you tie everything to them. Edit: Remember, this is just an RFC and the PR for fromObservable hasn't even merged yet. There is a high chance of the signal API itself changing.

Collapse
 
timsar2 profile image
timsar2 • Edited

I follow your content closely and thank you for sharing your experiences.
I was deciding to pick rx-angular and state-adapt to make zone-less my ionic mobile application (a mini social-media :D) until this days when signals come to angular preview.
So I'll start the app within a month later to learn and see more about signals from RFC.

Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more