Angular & signals. Everything you need to know.

Robin Goetz - Mar 2 '23 - - Dev Community

On February 15th, the Angular team opened the first PR that introduces signals to the framework. This early PR has gained huge momentum and sent the Angular community in a frenzy. Everyone is talking about signals, everyone is posting about signals, everyone is prototyping with signals.

The Angular team is doing an excellent job of pushing and explaining this new reactive primitive. They are making sure that developers have access to the tools they need to grasp this novel idea that will fundamentally alter how Angular apps function in the future. In their official Github conversation, they are diligently responding to questions and address community concerns. On twitter, they are providing the public with incredible bits of information. On live broadcasts, they engage in in-depth, hour-long technical discussions with signal thought leaders. They are doing everything they can to share their enthusiasm for and knowledge about this amazing new addition to the framework.

The only thing I have been missing so far is a compilation of all of this incredible information: One resource that digests all the materials available into a coherent story. A single article that teaches me what signals are, how they fit into the Angular story, what problems they address for the framework, and what it all means for me as an Angular developer ultimately using them to build my applications.

This article is my attempt to create such a resource.

Signals. A short introduction

So what are signals? A good starting point is to look at how Solid, a relatively new and increasingly popular framework that is built around signals describes them. Especially, the Angular team worked closely with its creator and CEO of Signals Ryan Carniato to develop Angular's version of them:

Signals are the cornerstone of reactivity in Solid. They contain values that change over time; when you change a signal's value, it automatically updates anything that uses it.

That seems pretty straight forward. A signal is a wrapper around a simple value that registers what depends on that value and notifies those dependents whenever its value changes.

A comparison often used to describe them to people familiar with RxJs are BehaviorSubjects, except for the need to manually subscribe/unsubscribe.

Signals always have a value, signals are side effect free, and signals are reactive, keeping their dependents in sync.

All together they deliver one simple model of how things change in Angular applications. Something that does not quite exist today. Let's take a look by what I mean by that.

It all started (about) 10 years ago

Angular's journey towards signals started 10 years ago. This might seem like a weird statement. We are talking about signals. THE framework that inspired the Angular team SolidJs (v1.0.0) was not released until June 28th, 2021. How could Angular's journey towards signals have started 10 years ago?

Well, that is about when v1.0.0 of Angular was released (13th of June 2012,) and with it an abundance of design choices. Design choices that each came with their own set of tradeoffs and (often significant) implications on developer and user experience.

These design choices were made based on the then current technology and the information available at that time. After 10 years of advancement in browser technology (there were no arrow functions until ES6 was finalized in 2015, async functions were added with ES8 in 2017), and hundreds of thousands of real world applications running on Angular, the Angular team decided to review those design decisions and see how they played out. As always with technology (and life in general) it turns out that some decisions turned out better than others.

Clearly, one of the best decisions was to build Angular on top of Typescript. What seems like an obvious choice now, was by no means one back in 2012. Typescript was just gaining popularity, many frontend developers never had to bother with type safety before and it seemed to unnecessarily slow things down. Angular became the first major framework to be built on top of Typescript. Today we know that type safety incredibly increases confidence and velocity in building applications at scale. But let's not get too sidetracked here.

Another decision was that the view state can not only live but also be mutated anywhere in the application. It does not matter if it is a simple boolean value in your component or an item of a list nested deeply in an object of a global service. Angular will detect the changes and update the DOM accordingly.

This gives developers incredible freedom to structure our applications in ways that make sense to us, and extract more complicated logic into services, all with minimal effort and concern about how our new data gets rendered in the DOM. No matter where we put and change our state, Angular's automatic global change detection will figure out the changes and the value will magically update in the DOM.

The challenges of automatic change detection

ZoneJs and when to check for changes

Unfortunately, in reality, it is not as easy as that. Those magic updates come at a price.

To enable its automatic change detection Angular depends on ZoneJs. ZoneJs is a library that patches native browser APIs and notifies the framework whenever a significant event occurs. This leads us to the first trade-off. Before anything can happen in an Angular application ZoneJs needs to load and run, which means that Angular sort of comes with a built-in performance deficit compared to frameworks that take a different approach to synchronize model changes and the DOM.

You might also be wondering what those significant events are that I mentioned above. The list includes event listeners, setTimeouts, Promises, and more. It turns out that to keep state that is mutable anywhere in sync with the DOM pretty much any browser event is significant. This means that often, even if we do not intend to even update the DOM, Angular will need to check the complete component tree and see if any of our data bindings' values are to be updated.

So on the one side of the story, Angular tends to overcheck and do unnecessary work trying to detect changes to the model. On the other side, the algorithm that determines which bindings changed comes with significant implications.

Unidirectional data flow or ExpressionChangedAfterItHasBeenCheckedError

We learned that there is an abundance of events that trigger Angular's change detection mechanism. To ensure that applications stay performant even with a great number of components in the DOM, the underlying mechanism must be incredibly efficient.

To ensure that, Angular's change detection runs in DOM order, and checks every binding between the model and the DOM only once. Again, after years and millions of lines of Angular it becomes clear that this way of checking for changes has its downsides.

The most important is that you cannot update any of your parents' data after it has

@Component({
  selector: 'app-child',
  standalone: true,
  imports: [CommonModule],
  template: `
    <h1>Hello</h1>
  `,
})
export class ChildComponent implements OnChanges {
  @Input()
  public changed = false;

  private parent = inject(ParentComponent);

  public ngOnChanges() {
    if (this.changed) {
      this.parent.text = 'from child';
    }
  }
}

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [ChildComponent],
  template: `
  {{text}}
    <app-child [changed]='true'/>
  `,
})
export class ParentComponent {
  text = 'from parent';
}
Enter fullscreen mode Exit fullscreen mode

This results in the famous ExpressionChangedAfterItHasBeenCheckedError:

ERROR
Error: NG0100: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'from parent'. Current value: 'from child'. Find more at https://angular.io/errors/NG0100
Enter fullscreen mode Exit fullscreen mode

Check out the Stackblitz to see the code in action.

Within a single change detection cycle we decided to go against the natural flow of the operation and update the parent after Angular has already checked its value.

You might say that this example seems a little artificial, but there are plenty of examples where the logical data flow of an application does not fit into the top-to-bottom nature of Angular's change detection. Even within Angular itself, we find this exact problem.

In the framework's FormsModule code, the validity of a parent is driven by the child. To avoid the ExpressionChangedAfterItHasBeenCheckedError when updating the parent's status. The operation had to be wrapped by an immediately resolved promise. This schedules a micro task with our update to the parents, which on completion triggers another change detection cycle, which can finally check the component tree in the allowed order and update the DOM.

I hope you are still following me.

Of course, there are techniques, like using a promise, that works around change detection's current limitations. However, they seem a little hacky and more importantly need a deep understanding of how change detection with ZoneJs works. In reality, Angular's global, automatic change detection does not always "just work."

OnPush, RxJs & why it doesn't solve all our (diamond) problems

If you are not familiar with OnPush change detection and/or want a refresher on how RxJs is commonly used, please view this super informative video by Joshua Morony that touches both on OnPush and RxJs and their importance for Angular development.

OnPush & async pipe. Performance improvement by addressing symptoms, not solving the root cause.

As mentioned above, Angular triggers change detection many times and while this is not an issue for simple applications, performance becomes a much more important topic once the amount of components and data bindings in the DOM increases.

One common way to improve performance in an Angular application is to use the OnPush change detection strategy and let Angular handle subscription management using the AsyncPipe.

The OnPush change detection strategy excludes the component marked as OnPush and all its children from the default change detection mechanism. There is a great article written by Angular University, which goes deeper into the mechanism. I highly recommend reading it.

Again, OnPush changes the way Angular detects changes for the component marked as onPush AND all its children.

The docs clearly state that:
Use the CheckOnce strategy, meaning that automatic change detection is deactivated until reactivated by setting the strategy to Default (CheckAlways). Change detection can still be explicitly invoked. This strategy applies to all child directives and cannot be overridden.

The implications of this are huge. If one of your components high up in the tree is OnPush, all other components will have to support OnPush also. This means that UI library authors have to build their library OnPush compatible because if your components do not support OnPush change detection you cannot guarantee that everyone in the Angular ecosystem can use your components.

RxJs. Powerful, declarative, and reactive programming with asynchronous streams

RxJS is a library for reactive programming using Observables, to make it easier to compose asynchronous or callback-based code.

Mike Pearson describes its incredible power perfectly in this tweet:

RxJs allows us to compose declarative, reactive pipelines using Observables that clearly show in the code what series of actions and mutations will happen in an asynchronous flow. It avoids race conditions and ultimately makes your code more readable and easier to reason about.

Angular introduced many of us to RxJs. It uses in many parts of the frameworks. Most famously in the HttpClient, which exposes the response of an HTTP-call through an Observable. Also, every Angular FormControl has a property called valueChanges. Which is a multicasting observable that emits an event every time the value of the control changes, in the UI or programmatically.

Combined with OnPush and Angular's AsyncPipe, a utility that binds our Observable directly to the template, these reactive pipelines have become a major pattern to build performant, race-condition-free, declarative Angular applications.

Streams are not behaviors. Why RxJs is not the solution.

However, if you take a closer look at how Angular uses Observables you notice a pattern. Angular uses RxJs for events, more specifically to expose streams of events. These event streams do not have a current value. Alex Rickabaugh gives a great way of thinking about this in their conversation with Ryan Carniato.

He is talking about click events specifically.

You cannot ask what is the current click event. That question does not make sense. You might ask: What is the most recent click event? However, this is a fundamentally different question. Behaviors (and Angular's signals fall into this definition of behaviors) always have a value. You will always be able to ask: What is this behavior's (signal's) current value? That is the most fundamental shortcoming of RxJs streams (Observables) as a solution to the challenges the Angular team is trying to address with their new reactive primitive.

In the same conversation, they do acknowledge that RxJs's BehaviorSubject is the closest thing the library offers to a signal. It always has a value. It can notify subscribers of changes to that value, and it exposes a way to get the current value and set a new value. However, it is not integrated into RxJs very well. As soon as pipe it through an operator (e.g. map()), it becomes an Observable and loses the connection that it was a BehaviorSubject that always has a current value. Also, RxJs has a plethora of powerful operators that can be used to map, join, debounce, etc. streams. For beginners, this power comes with a very steep learning curve. The Angular team needed something more focused to build their reactive primitive.

Picking the right tool for your (diamond) problems

Also, let's remind ourselves what the team set out to do in the first place. They want to introduce a reactive primitive that they can integrate with Angular's templating engine to notify the framework when the value bound to the view changes and needs to be updated in the DOM.

One of the major goals of such a primitive is glitch-free execution. Glitch-free execution means never allowing user code to see an intermediate state where only some reactive elements have been updated (by the time you run a reactive element, every source should be updated.)
Credit for this definition goes to Milo's awesome article on fine-grained reactive performance.

Again, RxJs only offers a solution that feels hacky at best. Let's look at the example below:

@Component({
  selector: 'normal',
  standalone: true,
  imports: [CommonModule],
  template: `
    <p>Hello from {{fullName$ | async}}!</p>
    <p>{{fullNameCounter}}</p>

    <button (click)="changeName()">Change Name</button>
  `,
})
export class NormalComponent {
  public firstName = new BehaviorSubject('Peter');
  public lastName = new BehaviorSubject('Parker');

  public fullNameCounter = 0;

  public fullName$ = combineLatest([this.firstName, this.lastName]).pipe(
    tap(() => {
      this.fullNameCounter++;
    }),
    map(([firstName, lastName]) => `${firstName} ${lastName}`)
  );

  public changeName() {
    this.firstName.next('Spider');
    this.lastName.next('Man');
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. We declare two BehaviorSubjects for firstName and lastName.
  2. We combine them into an observable fullName$ that simply concatenates the two.
  3. We declare a fullNameCounter which we increment every time our fullName$ Observable emits.
  4. We add a changeName function that we can trigger to set firstName and lastName to a different value AT THE SAME TIME.

Our component initially displays the following

Hello from Peter Parker!

1
Enter fullscreen mode Exit fullscreen mode

Once you click on the button the UI is updated to:

Hello from Spider Man!

3
Enter fullscreen mode Exit fullscreen mode

This means that our fullName$ observable actually emitted twice. One time for each change to firstName and lastName. While it happened so fast that we could not see it, this component actually rendered a intermediate state just to be instantly replaced by the final, correct state.

To avoid this and achieve our goal of glitch free execution, we need to add a debounceTime to our fullName$ to avoid the duplicate execution and end up with glitch-free execution.

@Component({
  selector: 'debounced',
  standalone: true,
  imports: [CommonModule],
  template: `
    <p>Hello from {{fullName$ | async}}!</p>
    <p>{{fullNameCounter}}</p>

    <button (click)="changeName()">Change Name</button>
  `,
})
export class DebouncedComponent {
  public firstName = new BehaviorSubject('Peter');
  public lastName = new BehaviorSubject('Parker');

  public fullNameCounter = 0;

  public fullName$ = combineLatest([this.firstName, this.lastName]).pipe(
    debounceTime(0),
    tap(() => {
      this.fullNameCounter++;
    }),
    map(([firstName, lastName]) => `${firstName} ${lastName}`)
  );

  public changeName() {
    this.firstName.next('Debounced Spider');
    this.lastName.next('Man');
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we see the counter only increasing by one at a time, indicating that we indeed will not render an intermediate state.

Hello from Peter Parker!

1
Enter fullscreen mode Exit fullscreen mode

Once you click on the button the UI is updated to:

Hello from Debounced Spider Man!

2
Enter fullscreen mode Exit fullscreen mode

Again, I encourage you to check out the working example.

An important take away is that combineLatest emitting once for every change to one of the observables it combines would also would also apply if Angular decided to make @Input()s observables. While this is of the most requested features of the community, you see that it comes with additional complexity.

So what does the exact same functionality look like with a signal primitive? I am glad you asked.

@Component({
  selector: 'my-app',
  standalone: true,
  template: `
    <p>{{ fullName() }}</p>
    <p>{{signalCounter}}</p>
    <button (click)="changeName()">Increase</button>
  `,
})
export class App {
  firstName = signal('Peter');
  lastName = signal('Parker');

  signalCounter = 0;

  fullName = computed(() => {
    this.signalCounter++;
    console.log('signal name change');
    return `${this.firstName()} ${this.lastName()}`;
  });

  changeName() {
    this.firstName.set('Signal Spider');
    this.lastName.set('Man');
  }
}
Enter fullscreen mode Exit fullscreen mode

Isn't the signal version's code so much easier and so much more straight forward.

Did you notice that even when you spam the changeName button, the count never goes over two?

Hello from Signal Spider Man!

2
Enter fullscreen mode Exit fullscreen mode

We will go into more detail on this later, but signals only notify consumers of their changes if their value changes. To get the same functionality in RxJs we would have to add yet another operator to our Observable: distinctUnitlChanged.

It is important to note that this does not mean that RxJs time is over. That it's on its way out of Angular and not useful anymore. RxJs shines by letting developers declare asynchronous, reactive streams. You can listen to an input's change events, debounce its values, switch them to parameters for an HTTP call, and map the response to the exact model that you need in the view, all in one place. This is simply not possible with signals.

All this boils down to one thing: While both are reactive by nature, RxJs and signals solve different issues. They are complementing each other, they are not substitutes for each other. Together, they will allow for much more powerful and straightforward Angular applications.

This Article by Mike Pearson goes into much more detail about the short comings of RxJs as a reactive primitive for Angular. It helped me immensely to understand why exactly RxJs & Observable inputs are not the solution Angular needs.

A new reactive primitive: signals

Now that we understand the issues the Angular team identified, and clarified why RxJs is not the solution to those challenges, we can move on and introduce the star of today's article: signals.

Let's revisit Solid's definition of a signal that we quickly introduced at the beginning of this article:

Signals are the cornerstone of reactivity in Solid. They contain values that change over time; when you change a signal's value, it automatically updates anything that uses it.

We will take a closer look at how signals achieve this later, but let's first focus on the API currently provided by the Angular team. Most of this is copied directly from the README provided here. It is an excellent resource and definitely worth the time spent reading it 1,2,3 times:

Angular Signals are zero-argument functions (() => T). When executed, they return the current value of the signal. Executing signals does not trigger side effects, though it may lazily recompute intermediate values (lazy memoization).

Particular contexts (such as template expressions) can be reactive. In such contexts, executing a signal will return the value, but also register the signal as a dependency of the context in question. The context's owner will then be notified if any of its signal dependencies produces a new value (usually, this results in the re-execution of those expressions to consume the new values).

Note: This is what makes signals so powerful as a mechanism of change detection and DOM synchronization. The affected template is directly notified. No walking of component trees and guessing when to re-check everything is necessary!

This context and getter function mechanism allows for signal dependencies of a context to be tracked automatically and implicitly. Users do not need to declare arrays of dependencies, nor does the set of dependencies of a particular context need to remain static across executions.

Note: Compare this to combineLatest and having to add each observable to the dependency array before being able to access their values later.

Settable signals: signal()

The signal() function produces a specific type of signal known as a SettableSignal. In addition to being a getter function, SettableSignals have an additional API for changing the value of the signal (along with notifying any dependents of the change). These include the .set operation for replacing the signal value, .update for deriving a new value, and .mutate for performing internal mutation of the current value. These are exposed as functions on the signal getter itself.

const counter = signal(0);

counter.set(2);
counter.update(count => count + 1);
Enter fullscreen mode Exit fullscreen mode

The signal value can be also updated in-place, using the dedicated .mutate method:

const todoList = signal<Todo[]>([]);

todoList.mutate(list => {
    list.push({title: 'One more task', completed: false});
});
Enter fullscreen mode Exit fullscreen mode

Note: We do not need immutability for signals to correctly notify their dependents of changes!!

Equality

The signal creation function one can, optionally, specify an equality comparator function. The comparator is used to decide whether the new supplied value is the same, or different, as compared to the current signal’s value.

If the equality function determines that 2 values are equal it will:

  • block update of signal’s value;
  • skip change propagation.

Declarative derived values: computed()

computed() creates a memoizing signal, which calculates its value from the values of some number of input signals.

const counter = signal(0);

// Automatically updates when `counter` changes:
const isEven = computed(() => counter() % 2 === 0);
Enter fullscreen mode Exit fullscreen mode

Because the calculation function used to create the computed is executed in a reactive context, any signals read by that calculation will be tracked as dependencies, and the value of the computed signal recalculated whenever any of those dependencies changes.

Similarly to signals, the computed can (optionally) specify an equality comparator function.

Side effects: effect()

effect() schedules and runs a side-effectful function inside a reactive context. Signal dependencies of this function are captured, and the side effect is re-executed whenever any of its dependencies produces a new value.

const counter = signal(0);
effect(() => console.log('The counter is:', counter()));
// The counter is: 0

counter.set(1);
// The counter is: 1
Enter fullscreen mode Exit fullscreen mode

Effects do not execute synchronously with the set (see the section on glitch-free execution below), but are scheduled and resolved by the framework. The exact timing of effects is unspecified.

Note: This might sound scary at first. As developers, unspecified behavior is always the enemy. However, in this case, this gives Angular the freedom to decide when exactly an effect is executed and it can use this to optimize performance for example.

Non reactive: untracked()

Note: This is currently not part of the official README. I do want to add this here to give you a complete overview of the API.

This prevents the wrapping computation from tracking any reads of the untracked signal. This means that even if the signal changes, the context is not notified of its change.

const counter0 = signal(0);
const counter1 = signal(0);

// Executes when `counter0` changes, not when `counter1` changes:
effect(() => console.log(counter0(), untracked(counter1));

counter0.set(1);
// logs 1 0
counter1.set(1);
// does not log
counter1.set(2);
// does not log
counter1.set(3);
// does not log
counter0.set(2);
// logs 2 3
Enter fullscreen mode Exit fullscreen mode

However, whenever the context is executed it will use the latest value of the untracked signal.

This is the current version of the API. Again, I want to encourage you to take the time to read the README on Github. It is absolutely phenomenal and an invaluable resource for everyone.

So how do signals work?

Again, I will mostly quote the awesome explanations of the README.

Producers and Consumers

Signals internally depend on two abstractions, Producer and Consumer. They are interfaces implemented by various parts of the reactivity system.

  • Producer represents values which can deliver change notifications, such as the various flavors of Signals.
  • Consumer represents a reactive context which may depend on some number of Producers.

In other words, Producers produce reactivity, and Consumers consume it.

Some concepts are both Producers and Consumers. For example, derived computed expressions consume other signals to produce new reactive values.

Both Producer and Consumer keep track of dependency Edges to each other. Producers are aware of which Consumers depend on their value, while Consumers are aware of all of the Producers on which they depend. These references are always bidirectional.

Together, they build a dependency graph that clearly lays out how the different nodes are related to each other.

How changes propagate through the dependency graph

We have already seen how RxJs Observables struggle to provide us with glitch free execution. We have also seen that signals elegantly solve this challenge.

Let's explore how they do that:

Push/Pull Algorithm

Angular Signals guarantees glitch-free execution by separating updates to the Producer/Consumer graph into two phases:

The first phase is performed eagerly when a Producer value is changed. This change notification is propagated through the graph, notifying Consumers which depend on the Producer of the potential update.

Crucially, during this first phase, no side effects are run, and no recomputation of intermediate or derived values is performed, only invalidation of cached values.

Once this change propagation has completed (synchronously), the second phase can begin. In this second phase, signal values may be read by the application or framework, triggering recomputation of any needed derived values which were previously invalidated.

We refer to this as the "push/pull" algorithm: "dirtiness" is eagerly pushed through the graph when a source signal is changed, but recalculation is performed lazily, only when values are pulled by reading their signals.

valueVersioning

This is probably the most complicated part of signals. First, we will take a look at the explanation provided by the Angular team. Then, we will take a look at an example.

Producers track a monotonically increasing valueVersion, representing the semantic identity of their value. The valueVersion is incremented when the Producer produces a semantically new value. The current valueVersion is saved into the dependency Edge structure when a Consumer reads from the Producer.

Before Consumers trigger their reactive operations (e.g. the side effect function for effects, or the recomputation for computeds), they poll their dependencies and ask for valueVersion to be refreshed if needed. For a computed, this will trigger recomputation of the value and the subsequent equality check, if the value is stale (which makes this polling a recursive process as the computed is also a Consumer which will poll its own Producers). If this recomputation produces a semantically changed value, valueVersion is incremented.

The Consumer can then compare the valueVersion of the new value with the one cached in its dependency Edge, to determine if that particular dependency really did change. By doing this for all Producers, the Consumer can determine that, if all valueVersions match, that no actual change to any dependency has occurred, and it can skip reacting to that change (e.g. skip running the side effect function).

Let's look at an example to better understand what is going on.

Let's assume we have the following code:

const counter = signal(0);
const isEven = computed(() => counter() % 2 === 0);
effect(() => console.log(isEven() ? 'even!' : 'odd!');

counter.set(1);
// logs odd!
counter.set(2);
// this is the change we are going to look at
Enter fullscreen mode Exit fullscreen mode

Push/Pull algorithm for setting signal to 2

This is what happens:

  1. The change to our SettableSignal sets off our push/pull algorithm.
  2. Our Producer counter pushes down its dirtiness and notifies its consumer isEven that its value is stale.
  3. isEven is also a Producer, which means that it also notifies its Consumers. In this case our console.log effect. This completes the push phase.
  4. effect now polls for isEven's current value. 5.isEven again polls for the newest version of counter. 6.counter has updated its value and valueVersion notifying isEven of its new state.
  5. isEven recomputes its own value, determines that its value changed, and increments its value version
  6. Finally, effect recognizes the new version value. Pulls the new value that changed to true, executes with the new value, and logs 'even!' to the console.

So let's look at what happens when we set the counter value to 4.

counter.set(4);
Enter fullscreen mode Exit fullscreen mode

Push/Pull algorithm for setting signal to 4

This is what happens:

  1. Again, the change to our SettableSignal to 4 sets off our push/pull algorithm.
  2. Our Producer counter pushes down its dirtiness and notifies its consumer isEven that its value is stale.
  3. isEven is also a Producer, which means that it also notifies its Consumers. In this case our console.log effect. This completes the push phase.
  4. effect now polls for isEven's current value.
  5. isEven again polls for the newest version of counter.
  6. counter has updated its value to 4 and its valueVersion 3 notifying isEven of its new state.
  7. isEven recomputes its own value, determines in fact its value did not change. Therefore, it keeps its value version the same.
  8. Finally, effect recognizes that isEven's valueVersion did not change. And it does not need to execute.

With the eager pushing of dirtiness down the dependency graph and the lazy pulling of values Consumer depend on signals to achieve exactly what we want: glitch-free execution.

A similar mechanism is used to track if the dependencies of a Producer went out of scope and can be garbage collected.
This means that you will never have to worry about unsubscribing from a signal. Memory leaks can simply not occur due to this way of dependency tracking!

Fine grained reactivity = fine grained change detection = performance explosion

With its own reactive primitive Angular has first class integration and can leverage all the benefits this new reactive way of doing things brings for change detection.

I think of rendering the template as an effect. Almost something like this:

const counter = signal(0);
const doubleCounter = computed(() => counter() * 2);

effect(() => renderTemplate(`
  <div>My counter is: ${counter()}</div>
  <div>My double counter is ${doubleCounter()}</div>
`));
Enter fullscreen mode Exit fullscreen mode

We render the template in an effect, which becomes a consumer of each signal used in the template. These signals used will notify us when they change and we will know exactly when to re-render our template.

To better understand how incredibly efficient fine-grained reactivity and signals are when it comes to keeping the DOM and model in sync becomes clear when we compare the current change detection mechanism and the new signal-enabled mechanism.

Before we start. This is what every symbol means in the following diagrams
Symbols explained

How Angular currently detects changes

Current Angular change detection mechanism

Assume we have an application that has multiple components. Some of them have models that logically depend on each other. Others do not. Their views are all connected through parent/child relationships within the DOM tree.

Angular creates a similar tree with its top-down dirty checking change detection algorithm. Regardless of how the data of components depend on each other, the framework creates parent/child relationships between the different components.

Then, for every change detection cycle, it walks down the tree one time and compares the bindings' old value with the new value of the model. Every single binding will be checked exactly one time.

We see that a model has changed inside our component tree, symbolized by the orange circle. The model itself has no logical dependency on any other component. As we enter a new change detection cycle the following happens:

  • Angular starts at the top of the tree and begins to check that node
  • It then continues to walk along the tree to determine which components need to be updated
  • Finally, it reaches the component in which the model has changed.
  • The equality check fails and the DOM is updated.

These steps happen every time change detection is triggered.

Let's see how signal-based change detection works.

Signal powered change detection

Signal based change detection

We have the same application with multiple components. Some models logically depend on each other, while others do not. Their views are all connected through parent/child relationships within the DOM tree.

With signals, there is no need to create a tree that enables change detection. The signals used in the template notify it of their changes. That's why in this abstraction the change detection arrow is directly connected to the DOM node.

So what happens when the model changes?

The template is notified of this change and the DOM updates.

That's it.

Mind blown

No more top-down walking of a graph.

No more unnecessary comparisons.

The notification mechanism that informs the framework when to update the view is built into signals.

Mind blown!

One can only imagine the performance improvements that come from this fine grained reactivity.

signals + RxJs = <3

I hope at this point you are just as excited for signals as I am!

I want to end this article by reiterating that signals and RxJs complement each other.

Many of the applications built with OnPush, RxJs, and the async pipe are already set up to take complete advantage of the performance increase provided by signals. The async pipe will be replaced by a signal. OnPush will completely disappear as signals notify the framework whenever a DOM update is necessary. RxJs will shine by focusing on what it does best: Modeling complex, asynchronous streams in a declarative way.

Together they will power the Angular applications of the future.

Ready. Set. Go.

It truly is an exciting time in the Angular community. I am 1000% convinced that signals will significantly improve developer and user experience of Angular applications. They provide one simple model to update Angular's views. They are performant, reactive, and will soon be an indispensable part of Angular.

I hope you are now equipped with the knowledge to take full advantage of signals when they land later this year.

As always, do you have any further questions or suggestions for blog posts? Are you excited about signals or do you still see issues the team needs to address? I am curious to hear your thoughts. Please don't hesitate to leave a comment or send me a message.

Finally, if you liked this article feel free to like and share it with others. If you enjoy my content follow me on Twitter or Github.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .