Demystifying the push & pull nature of Angular Signals

Let's take a deep dive into Angular signals and how they can be understood using the primitive push & pull concepts to make sense of all their behaviors from setting a value all the way to effects and laziness!

emoji_objects emoji_objects emoji_objects
Tomas Trajan

Tomas Trajan

@tomastrajan

11.04.2023

10 min read

Demystifying the push & pull nature of Angular Signals
share

🤖 prompts & design by Tomas Trajan, gen by MidJouney

UPDATE 11th Dec 2023: As the signals internal implementation keeps evolving and improving, some of the behaviors described in this article might not be 100% accurate anymore. Instead, check out my new Angular Signals In-depth video on YouTube which covers the behavior of latest and greatest Angular Signals!

Angular Signals are THE HYPE at the moment and for a very good reason!

The promise of new better officially sanctioned way to manage state in our Angular applications is something we have been waiting for since the first Angular release!

Throw in improved DX, better ergonomics and signal based inputs and even the most skeptical developers will agree that Angular team is onto something objectively better and amazing!

But as with everything new, there is going to be transitory period and a bit of friction while getting acquainted with the new APIs and developing new mental models of how signals work in practice!

A teaser

It all started with a little Twitter Angular Signals Quiz…

Even though most folks called the correct answer in the replies, there was definitely a bit of uncertainty about how and why exactly is zero the correct answer as we’re obviously updating the value of the source signal multiple times…

A new piece of puzzle

Signals are definitely reminiscent of the revolution caused by the introduction of RxJs based APIs back in the day when Angular (2) was first released.

One of the thing that helped me the most when first learning about RxJs was this diagram from the official RxJs documentation!

Push / **pull** vs Single / multi diagram

It helped me to organize my scattered implicit know-how about “how things work” with just a couple of clean cut concepts and their combinations.

Rewinding back to present, this approach proved to be very helpful when wrapping my head around the new Angular signals and their sometimes not so obvious or intuitive behaviors!

Let’s provide a short summary for each concept, and then we’re going to figure out what is the rightful place of the Angular signals in this chart.

  • single / multi — self-explanatory, we’re going to receive single or multiple values (multiple being anything from zero to infinity)
  • pull — we have to “pull” the result out of the thing by explicitly calling it
  • push — the thing will “push” a new result to us once ready, it will call a handler we have provided

With this knowledge, we can see that the

  • function — we have to call it (pull value out of it) and it delivers single result per call
  • iterator — we have to call it, and it delivers multiple results, one per each call (pull)
  • promise — it will call our handler once the value is ready (pushes value to us)
  • observable — it will call our handler, zero to n times whenever a next value is ready (pushes values to us)

So what about the signals?!

Let’s start with what was communicated at in the official Angular Signals RFC

A signal is a wrapper around a value, which is capable of notifying interested consumers when that value changes.

So far, this sounds like a push capable of multiple notifications about possible change…

Because reading a signal is done through a getter rather than accessing a plain variable or value, signals are able to keep track of where they’re being read.

At the same time “reading a signal (value) is done through a getter” which sounds a lot like a pull capable of multiple calls…

Which brings us to…

Push / **pull** vs Single / multi diagram with Angular Signals

Angular Signals are a push / pull based reactive primitive for Angular!

Great, we’re done, case closed… But what does that mean in practice?!

Plain signals

Let’s start by creating the most basic signal possible…

const count = signal(0);

The created signal has initial value of 0 and is stored in the count variable.

Nothing is really happening as of now, so let’s try to call it…

console.log(count()); // 0

Calling of signal will return its current value synchronously (pull) and we’re able to call it as many times as we want (multiple). Each call will return signal value available at that point of time!

So what about the following…

const count = signal(0);

console.log(count()); // 0

count.set(1);
count.set(2);

As we learned previously:

  • updating signal value (for example using set method) will send a push notification to all the signal consumers that its value might have changed
  • signals are able to keep track of where they’re being read (who the consumers are)
  • In the example above, the count signal sends 2 push notifications about possible change, BUT we’re not actually ever reading its value after that happens (pull) so in the example above these notifications would NOT lead to any kind of read / refresh behavior!

Signals in Angular templates

Let’s create a more realistic example with an actual Angular component…

@Component({
  template: `
    <p>Count: {{ count() }}</p>
    <button (click)="increment()">Increment</button>
  `,
})
export class CounterComponent {
  counter = signal(0);

  increment() {
    this.counter.update((current) => current + 1);
  }
}

The component will render initial 0 and a user then clicks on the button two times. After that the component will render 2 which is correct but why?!

As we have learned, the setting (or updating) of the signal value sends push notification that the signal value might have changed but nothing is going to happen and some consumer has to then still pull current value out of the signal with explicit call…

Which means the component somehow knows that it should pull the current value and more so it does it at the correct time!

As it turns out, in the example above, this correct behavior has nothing to do with the signals themselves!

The pull of the current value out of the signal is caused by the re-run of the template bindings caused by the good old Angular change detection triggered by the button click with the help of zone.js!

As we will see later, this is going to change in the Angular signals based components!

Angular signals send eager push notification to the registered consumers (who read signal value) that the signal value might have changed but nothing is going to happen till the consumer pulls current value out of the signal by calling its getter!



Follow me on Twitter because that way you will never miss new Angular, NgRx, RxJs and NX blog posts, news and other cool frontend stuff!😉



Computed Angular Signals

Now it’s time to talk about the computed() signals which are best suited to implementation of the reactive derived state!

They are bound to replace and simplify logic previously solved with the help of ngOnChanges() lifecycle hook or more advanced solutions like BehaviorSubject / ComponentStore patterns…

computed(() => {
  return counter() % 2 === 0;
});

Any signal called in the computation (the computed implementation function) will register the computed signal as its dependency in the reactive graph. Or in other words, the counter signal will be registered as a producer and the computed signal as a consumer in the edge connecting the two in the underlying data structure.

Let’s explore how it will behave under multiple common circumstances…

First, we’re going to trigger couple of updates to the counter signal using the .set() method, eg counter.set(2).

These updates will lead to the *push** behavior, but instead of new value, the producer signal counter will send only **push** a notification that something might have changed to the consumer computed signal!

Because of that, the computed signal will only mark state as stale and the computation will NOT re-run just yet!

So did the computation run at all?!

In our original example, we have never stored the reference to the computed signal in any variable and therefore also never called it.

This means that even though the computed signals lives as a part of reactive dependency graph it will in fact never run and only receive push notifications about the potential changes!

Let’s adjust the example and store the computed signal in the isEven constant.

const isEven = computed(() => {
  return counter() % 2 === 0;
});

The behavior will be the same as before because we haven’t called or pulled the value out of the computed isEven signal just yet!

Now it’s time to finally call the isEven() signal somewhere, for example in the template of an Angular component. Similar to the previous basic signal example:

  • the change to the producer counter signal will be caused by some user interaction (eg button click)
  • the resulting zone.js based change detection will re-run template bindings of the component
  • isEven() signal will be called and check if it received any push notification since its last run and re-run the computation which will then pull the current value from all the referenced producer signals
  • the resulting computed value will be then displayed in the template

To summarize…

Computed Angular signals are eagerly added as a part of the reactive dependency graph and receive eager push notifications about potential change in the referenced producer signals!

At the same time, computed Angular signals will postpone execution (lazy) of the computation function till they are called explicitly and then pull the most recent value from the producer signals that might have changed!

Computed Angular Signal Cheat Sheet

  • lazy
  • signals referenced in the computation function body are registered as producers
  • producers send eager push notifications to computed consumer that the values might have changed and that the computed state might be stale
  • computed signal must be called explicitly to run
  • computation will only re-run if stale
  • computed signal will pull current (latest) value from referenced producers if stale (only once even if it received multiple notifications)

Angular Signals Effects

The effect() represents the most advanced Angular signals API which allows us to run side-effects as a reaction to change in referenced signals.

Let’s see it in action in the following example.

@Component({
  /* ... */
})
export class EffectExampleComponent {
  constructor() {
    const counter = signal(0);

    effect(() => {
      console.log('Effect runs with: ', counter());
    });
  }
}

Notice that this example uses Angular component as a wrapper and reason for that is that effect() is more tightly integrated with Angular core, especially it needs to run in injection context (constructor time) because it is injecting DestroyRef behind the scenes to provide self cleanup out of the box.

We are creating a new counter signal with an initial value of 0 and an effect which should log counter value to the console whenever it changes.

What do you think will be the console output if we started Angular application with exactly such component?

As it turns out, the output will be Effect runs with: 0 because effects state is set to dirty at its creation which will lead to a first pull of the values from the producer signals referenced in the effect implementation.

The effects execution is currently tied to the Angular’s logic to refresh particular view (and therefore change detection), so this first run would correspond to initial render of the parent component.

This also represents first major difference in comparison with computed which is completely lazy and will not execute until we call it explicitly!

Let’s change things a bit more to uncover other Angular signal effect properties.

@Component({
  /* ... */
})
export class EffectExampleComponent {
  constructor() {
    const counter = signal(0);

    effect(() => {
      console.log('Effect runs with: ', counter());
    });

    counter.set(1);
    counter.set(2);
    counter.update((current) => current + 1);
    counter.update((current) => current + 1);
  }
}

How about now? We’re updating the value of the counter signal using both set and update methods synchronously in the constructor of the component.

As established previously, calling these methods will cause the producer counter signal to push multiple notification to the effect in the role of a consumer that its value might have changed, but not the values themselves!

If we tried to run this example in an actual Angular application, the console will have only a single line of output and the line would say

Effect runs with: 4

The reason for that is that all those producer updates will happen before the first and only evaluation of the effect (as it’s initially marked as dirty) and executed on components view refresh which happens after the execution of the constructor.

Angular signals effects receive eager push notifications that referenced signals might have changed, and *pull the values from those signals when Angular runs change detection

* soon we’re going to see that it’s a little but more nuanced

Now it’s time to make our example more dynamic by introducing user interaction (and change detection)!

@Component({
  template: `<button (click)="update()">Update</update>`,
})
export class EffectExampleComponent {
  counter = signal(0);

  constructor() {
    effect(() => {
      console.log('Effect runs with: ', this.counter());
    });
    // logs "Effect runs with: 0" when component is initialy rendered
  }

  update() {
    this.counter.update((current) => current + 1);
    this.counter.update((current) => current + 1);
    this.counter.update((current) => current + 1);
  }
}

So what is going to happen if user clicks on the “Update” button?

As we’ve established, signal updates send sync push notification to the consumers ( the effect in our case ) that the value might have changed.

We also know that in zone.js based Angular applications every DOM event, which means also the (click) event triggers app wide change detection which will lead to the re-run of the effect implementation function which is going to pull the value from the signal at that moment.

This means that after the first user click, we’re going to see a single Effect runs with: 3 output in the console!

This behaves exactly the same as the previous effect updates in the constructor so where is that nuanced behavior which we mentioned earlier?!

Angular Signals Effects are Push -> Poll -> Pull

Let’s adjust our example one more time by introducing a computed signal an intermediary node of our reactive dependency graph.

The effect now depends on computed which depends on the counter signal itself…

@Component({
  template: `<button (click)="update()">Update</update>`,
})
export class EffectExampleComponent {
  counter = signal(0);

  constructor() {
    const isEven = computed(() => {
      return this.counter() % 2 === 0;
    });

    effect(() => {
      console.log('Effect runs with: ', isEven());
    });
    // logs "Effect runs with: true" when component is initialy rendered
  }

  update() {
    this.counter.update((current) => current + 2); // notice + 2
  }
}

As previously the effect will log initial true as the initial counter value of zero is in fact even and the effect is marked as dirty when created…

So what is going to happen when a user clicks the update button?

  • the counter signal will send a push notification to the consumer computed isEven signal which will forward the notification also to its own consumer, the effect
  • click will also trigger zone.js based change detection which will lead to re-execution of the effect ( as a part of refreshView call)
  • as the effect starts running, it will start its execution as it was previously marked as dirty because of the received push notification
  • effect will then poll producer isEven computed signal and realize that the value of isEven has in fact NOT changed!
  • effect will abort its execution and NOT pull the current value of isEven nor log any output to the console!
  • any subsequent click will lead to the same behavior (0 + 2 = 2, + 2 = 4, … which are all even so the value of isEven computed signal won't change anymore)

Angular Signal Effect Cheat Sheet

  • effect start as dirty and will run at least once (in realistic scenarios because its parent component is change detected on creation)
  • effects are currently scheduled to run when Angular change detects and refreshes view (of a component) so it will run with the most recent state of referenced signals even if multiple sync updates to referenced signals has happened in between
  • running effect polls referenced producer signals if their value has changed before it pulls their value and re-runs and therefore won’t re-run if the value stayed the same, for base signals value always changes when push notification is sent so poll always resolves to true, for computed the push notification is sent but value might have stayed the same so poll can resolve to false and effect won't run
  • effect supports cleanup / cancellation of in progress operation, with the onCleanup argument passed to the effect implementation function
  • effect itself is cleaned up automatically when the parent component or service is destroyed (DestroyRef)

As we can see, Angular signals effect comes with some behaviors which might not be completely intuitive from the get go. This explanation might feel a bit abstract, and therefore I have created a StackBlitz example which showcases these behaviors in a live Angular application!

Angular Signals Components

In this article, all the previous examples assumed zone.js based change detection with all its consequences. If you have been paying attention to the Angular Signals RFC, you might be aware that Angular is also going to introduce new signals based components with signals: true flag (similar to standalone: true flag).

Setting your component to be signals based will switch it to signals based change detection so it will become zone-less and change detection will be triggered any time a signal which is consumed in this components template receive push notification that it might have changed.

This behavior will be implemented either directly with the effect (or something very similar to the signals effect) and follow the whole push -> poll -> pull dance as described above!

There are many other great ways to build your Angular signals effects mental model, especially this talk by Angular core team member Pawel from NG-BE 2023 so don’t hesitate and check it out!

Angular Signals are awesome!

I hope you have enjoyed learning about the push & pull nature of the Angular signals and will now feel comfortable and confident when introducing signals into our Angular codebases!

The newly acquired know-how will help you to make sense of why signals behave the way they do, for all their behaviors from setting a value all the way to effects and laziness!

Also, don’t hesitate to ping me if you have any questions using the article responses or Twitter DMs @tomastrajan

Do you enjoy the theme of the code preview? Explore our brand new theme plugin

Skol - the ultimate IDE theme

Skol - the ultimate IDE theme

Northern lights feeling straight to your IDE. A simple but powerful dark theme that looks great and relaxes your eyes.

Do you enjoy the content and would like to learn more about how to ensure long term maintainability of your Angular application?

Angular Enterprise Architecture eBook

Angular Enterprise Architecture eBook

Learn how to architect a new or existing enterprise grade Angular application with a bulletproof tooling based automated architecture validation.

This will ensure that Your project stays maintainable, extendable and therefore with high delivery velocity over the whole project lifetime!

Get notified
about new blog posts

Sign up for Angular Experts Content Updates & News and you'll get notified whenever I release a new article about Angular, Ngrx, RxJs or other interesting Frontend topics!

We will never share your email with anyone else and you can unsubscribe at any time!

Emails may include additional promotional content, for more details see our Privacy policy.

Responses & comments

Do not hesitate to ask questions and share your own experience and perspective with the topic

Tomas Trajan - GDE for Angular & Web Technologies

Tomas Trajan

GDE for Angular & Web Technologies

I help developer teams deliver successful Angular applications through training and consulting with focus on NgRx, RxJs and Nx!

Ein Google-Developer expert für Angular und Webtechnologien, der als Berater und Angular-Trainer arbeitet. Derzeit unterstützt er weltweit Teams in Unternehmen bei der Implementierung von Kernfunktionalitäten, Architekturen, der Einführung von Best Practices, und der Optimierung von Arbeitsabläufen.

Tomas ist ständig bestrebt, seinen Kunden und der breiteren Entwicklergemeinschaft einen maximalen Nutzen zu bieten. Seine Arbeit wird durch eine umfangreiche Erfolgsbilanz unterstrichen, in der er populäre Fachartikel veröffentlicht, Vorträge auf internationalen Konferenzen und Meetups hält und zu Open-Source-Projekten beiträgt.

52

Blog posts

4.7M

Blog views

3.5K

Github stars

612

Trained developers

39

Given talks

8

Capacity to eat another cake

You might also like

Check out following blog posts from Angular Experts to learn even more about related topics like Modern Angular or Signals !

Stärken Sie Ihr Team mit unserer umfassenden Erfahrung

Unsere Angular Experten haben viele Jahre damit verbracht, Unternehmen und Startups zu beraten, Workshops und Tutorials zu leiten und umfangreiche Open-Source-Ressourcen zu pflegen. Wir sind sehr stolz auf unsere Erfahrung im Bereich des modernen Frontends und würden uns freuen auch Ihrem Unternehmen zum Aufschwung zu verhelfen.

or