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!
Tomas Trajan
@tomastrajan
Apr 11, 2023
10 min read
š¤ 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ā¦
š¦ #Angular Signals Quiz Time!
ā Tomas Trajan (@tomastrajan) March 31, 2023
š§® Computed edition
Given the following snippet,
how many times will the console.log
print the 'called'?
Bonus points for explaining why š pic.twitter.com/FBWyPlKnXK
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!
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ā¦
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 thecomputed
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 injectingDestroyRef
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 computedisEven
signal which will forward the notification also to its own consumer, theeffect
- click will also trigger
zone.js
based change detection which will lead to re-execution of the effect ( as a part ofrefreshView
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 ofisEven
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
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
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 we release a new blog posts 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!
Responses & comments
Do not hesitate to ask questions and share your own experience and perspective with the topic
Tomas Trajan
Google Developer Expert (GDE)
for Angular & Web Technologies
I help developer teams deliver successful Angular applications through training and consulting with focus on Architecture and State managements with NgRx!
A Google Developer Expert for Angular & Web Technologies working as a consultant and Angular trainer. Currently empowering teams in enterprise organizations worldwide by implementing core functionality and architecture, introducing best practices, sharing know-how and optimizing workflows.
Tomas strives continually to provide maximum value for customers and the wider developer community alike. His work is underlined by a vast track record of publishing popular industry articles, leading talks at international conferences and meetups, and contributing to open-source projects.
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 !
Top 10 Angular Architecture Mistakes You Really Want To Avoid
In 2024, Angular keeps changing for better with ever increasing pace, but the big picture remains the same which makes architecture know-how timeless and well worth your time!
Tomas Trajan
@tomastrajan
Sep 10, 2024
15 min read
Total guide to lazy loading with Angular @defer
Learn everything about the lazy loading of standalone components with Angular @defer block including best practices, deep dive on all interactions and gotchas including a live demo!
Tomas Trajan
@tomastrajan
Nov 14, 2023
13 min read
Angular Control Flow
A new modern way of writing ifs, if-else, switch-cases, and even loops in Angular templates!
Kevin Kreuzer
@kreuzercode
Oct 24, 2023
5 min read
Empower your team with our extensive experience
Angular Experts have spent many years consulting with enterprises and startups alike, leading workshops and tutorials, and maintaining rich open source resources. We take great pride in our experience in modern front-end and would be thrilled to help your business boom