DEV Community

Cover image for Signals make Angular MUCH easier
Mike Pearson
Mike Pearson

Posted on

Signals make Angular MUCH easier

YouTube

Why shouldn't RxJS do everything?

RxJS is amazing, but it has limitations. Consider a counter implemented using a simple variation of the "Subject in a Service" approach:

export class CounterService {
  count$ = new BehaviorSubject(0);

  increment() {
    this.count$.next(this.count$.value + 1);
  }
}
Enter fullscreen mode Exit fullscreen mode

Now multiple components can share and react to this state by subscribing to count$:

Image description

Now let's add some derived states with the RxJS map and combineLatest operators:

  count$ = new BehaviorSubject(1000);

  double$ = this.count$.pipe(map((count) => count * 2));
  triple$ = this.count$.pipe(map((count) => count * 3));

  combined$ = combineLatest([this.double$, this.triple$]).pipe(
    map(([double, triple]) => double + triple)
  );

  over9000$ = this.combined$.pipe(map((combined) => combined > 9000));

  message$ = this.over9000$.pipe(
    map((over9000) => (over9000 ? "It's over 9000!" : "It's under 9000."))
  );
Enter fullscreen mode Exit fullscreen mode

Here's a diagram of these reactive relationships:

Image description

Here's what that looks like:

Shared Counter State 2

Isn't this easy? RxJS just takes care of everything for us. There's probably nothing wrong with this.

Actually there is. Let's put a console log inside the map for message$ and see what happens when we increment the count once.

  message$ = this.over9000$.pipe(
    map((over9000) => {
      console.log('Calculating message$', over9000);
      return over9000 ? "It's over 9000!" : "It's under 9000.";
    })
  );
Enter fullscreen mode Exit fullscreen mode

Image description

Why did it run 4 times? We only incremented the count once. That's not efficient.

Something weird is going on. Let's put console logs inside each observable so we can get a view into everything happening. And think for a minute about what we should expect. We have a single event, and 5 derived states: double$, triple$, combined$, over9000$, and message$. Shouldn't we see 5 console logs? Well, here's what we actually get:

Image description

It's over 9000!!! We just implemented our feature in the simplest way possible, and this is what RxJS gave us. This is 40 logs, or 8x what it should be.

We need to understand how subscriptions work. We have 2 components subscribing to several of these observables. Here I've added a colored line for each subscription:

Image description

Each subscription gets passed all the way up to the top of the chain. If you count the number of blue and green lines next to double$ and triple, it's 8 each. That's the number of console logs for each of those. combined$ has 12 lines around it (because of the branching), and 12 logs. But message$ has 2 lines and not 2 but 4 console logs, and over9000$ has 4 lines but 8 console logs. That's because each of those lines ends up splitting into 2 lines up at the combineLatest.

We have to learn more operators to deal with these problems: map and distinctUntilChanged (sometimes with a comparator), combineLatest and debounceTime, and shareReplay. Actually, not shareReplay, more like publishReplay and refCount. Or actually, merge, NEVER, share and ReplaySubject (more on these later). The really crazy thing is that most people aren't even aware of all these issues. It takes some painful experiences to learn that these operators are necessary.

But asking everyone to avoid the numerous RxJS pitfalls, become intimately familiar with how subscriptions work, and learn all these operators, all just for basic derived state, is absurd. And, these operators increase bundle size and do work at runtime. Creating a custom operator doesn't fix that.

So, while RxJS is amazing for managing asynchronous event streams, it is inefficient and difficult to use for synchronizing states.

How about selectors?

Selectors are pretty efficient at computing derived states.

But I never liked their syntax:

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

So for StateAdapt I came up with new syntax:

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

But selectors require a state management library with a global state object, which makes them impossible to integrate tightly with framework APIs, such as component inputs.

Signals

Angular needed a reactive primitive of its own, and out of all the options, signals were the best choice for synchronization.

Let's implement our counter with Angular signals:

  count = signal(1000);

  double = computed(() => this.count() * 2);
  triple = computed(() => this.count() * 3);

  combined = computed(() => this.double() + this.triple());

  over9000 = computed(() => this.combined() > 9000);

  message = computed(() =>
    this.over9000() ? "It's over 9000!" : "It's under 9000."
  );
Enter fullscreen mode Exit fullscreen mode

Now when we click, we get the 5 expected logs:

Image description

It's more efficient than even optimized RxJS, and we only needed one "operator": computed.

The Angular team did an amazing job with the implementation, too. If you want to learn more about how it works, I recommend this interview they did with Ryan Carniato.

Problems with signals

Signals are awesome, but like RxJS, they have limitations:

  1. Asynchronous Reactivity
  2. Eager & Stale State

These will be the topics of my next articles.

Top comments (17)

Collapse
 
michaeltharrington profile image
Michael Tharrington

Heyo Mike! Great post. 🙌

In case ya didn't already know, DEV actually allows folks to embed YouTube & Vimeo videos using the following syntax:

{% embed https://... %}

You def don't need to embed the YouTube video at the top if you'd rather not, but I just wanted to let you know how in case you'd like to.

Hope this is helpful and thanks for sharing this awesome post! 🙂

Collapse
 
stealthmusic profile image
Jan Wedel

I‘m really looking forward to signals in Angular. TBH, so never liked rxjs because it’s a complete and complex DSL on top of JS/TS. Not only you have to learn what all these operators do, but you also have to know their implementation as you showed because it has a lot of weird side effects. There is A LOT to do wrong. Signals on the other hand are pretty simple to understand as there are just a few building blocks to learn and it works more intuitively.

And as a plus, it will bring a lot of performance when we can get rid of ZoneJS for change detection.

It will not help in all cases and sometimes you need to use rxjs but I guess that’s acceptable, especially because you can transform signals to and from observables.

Collapse
 
mfp22 profile image
Mike Pearson

There there is still a place for RxJS still for sure. My next article is called "RxJS can save your codebase." But I'm excited at how signals will improve most apps by a lot.

Collapse
 
stealthmusic profile image
Jan Wedel

I am looking forward to it. RxJS is very powerful for sure but it has a steep learning curve and the benefit / complexity ratio is not very high IMO. The problem is, there no better alternatives in the JS ecosystem AFAIK. So we have to live with it. I would love to see some better asynch primitives in JS. Async/await is nice but it suffers from the necessity to use it up to the top of the call chain.

Collapse
 
spock123 profile image
Lars Rye Jeppesen

RXJS (and state management systems like NGRX) will never go away, it's not that Signals can replace them. Think about complex async side effects that Signals cannot handle.

I think best-of-all worlds is to use NgRX and then expose all selectors as Signals, using the method provided by the NGRX team. I just implemented an app using this approach (where my facades expose all the selectors as signals) and it's a game changing developer experience.

Thread Thread
 
mfp22 profile image
Mike Pearson

It's fine but NgRx blocks the benefits of RxJS I talk about in the 3rd article in this series.

Collapse
 
lalves91 profile image
LAlves91

Greetings! Awesome article!

One question: the four logs that happened for message$, were them only caused by the click or did it include the first value emitted by the BehaviorSubject? I'm trying to make sense of all the logs here and this would "kinda" fit with my assumptions (at least up to the combined$...after that I'm not sure hahahah).

Considering each UI usage, as well as the map operator, generates a subscription:

  • message$ has 2 subscriptions (two direct ones from UI);
  • over9000$ has 4 subscriptions (two direct ones from UI, and two from UI subscriptions of message$);
  • combined$ has 6 subscriptions (two direct ones from UI, two from UI subscriptions of message$ and two from UI subscriptions of over9000$);

If the logs are showing both the first value emitted by the BehaviorSubject and the one after clicking the button, the amount of logs for these three would lign up (4 for message$, 8 for over9000$ and 12 for combined$).

The problem in my logic was with double$ and triple$. But when we have half of a working logic, sometimes we want the rest to fit hahahah. So I thought that maybe this cascading of subscription doesn't apply for the combineLatest operator, which could mean double$ and triple$ each only have four subscriptions (two direct UI usages and two direct combined$ usages on UI). Considering two values emitted by the Subject, 8 logs.

Sorry for the really long comment, problems like this one drives me crazy until I understand what's going on hahahahah

Thank you! Keep up the great work!

Collapse
 
mfp22 profile image
Mike Pearson

Thanks for the comment :)

These logs only occurred immediately after I clicked the plus button. There were logs before it with the initial value, but I cleared the console and all of these happened after the click. I triple-checked it, because I myself was surprised.

This article has been shared a lot now, so maybe Ben Lesh will drop by and tell me I'm an idiot about something. I'm sure there's something missing in the way I described it. It seems like there's always another level of understanding with RxJS. But as I said in another comment, it can also be helpful to look at the source code of the operators themselves and see what's going on under the hood.

Collapse
 
wojky profile image
Kamil Wojkowski

Such a first thought, why do we create so many dependent streams? We get exactly the same result by simply creating one source of truth:

  vm$ = this.count$.pipe(map(count => {
      const double = count *2;
      const triple = count * 3;
      const combined = double + triple;
      const over9000 = combined > 9000;
      const message = over9000 ? "It's over 9000!" : "It's under 9000."

      return { double, triple, combined, over9000, message};
  }))
Enter fullscreen mode Exit fullscreen mode

PS. can't wait once the signals go in permanently!

Collapse
 
mfp22 profile image
Mike Pearson

Thanks for the idea. I think I don't do it this way because it couples states that don't need to be related, and memoization/distinctUntilChanged won't be easy to apply to any of them, or the whole thing (new objects created every time). Each derived state is its own concern and should be able to be easily moved somewhere else.

Collapse
 
raj_sekhar profile image
Raj Sekhar

Hey @wojky
thats a really nice way tio rethink about the solution.

Collapse
 
timsar2 profile image
timsar2

I think after 2 years of watching stateAdapte, It's time for me to start my next project with stateAdapte

Collapse
 
mfp22 profile image
Mike Pearson

It should be fun! It is for me anyway.

1.0.1 is going to have a bugfix for the entity adapter for non 'id' ID props, then I believe 1.1.0 is going to include a way to get a signal for each selector without eagerly subscribing to each selector. It's going to be neat I hope.

Collapse
 
baypanic profile image
Glib Zaycev

Great article, thanks!
I don't quite get why the rxjs implementation was producing such amount of events though. Probably i am not aware of some detaisl of the implementation of rxjs streams, can you advice something to read about it, please?

Collapse
 
mfp22 profile image
Mike Pearson

I don't understand it fully, but RxJS operators just pass on their subscribers normally. So it's as if each final subscriber is directly subscribed to the original sources. The source code of map is pretty simple as an example to look at. github.com/ReactiveX/rxjs/blob/mas...

Collapse
 
govindappaarun profile image
Arun Kumar Govindappa

all the images seems to be missing @mfp22 , can you fix it please

Collapse
 
senms profile image
Sen

Great