DEV Community

Cover image for Angular & signals. Everything you need to know.
Robin Goetz for This is Angular

Posted on • Updated on

Angular & signals. Everything you need to know.

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.

Top comments (49)

Collapse
 
ivanivanyuk1993 profile image
Ivan Ivanyuk • Edited

Added comment github.com/angular/angular/pull/49...

If we don't preprocess function in computed at compile-time or use some form of reflection to atomically notify only affected signals, won't it require O(n) change checks, where n is a quantity of signals in application, effectively making it have same algorithmic complexity as ChangeDetectionStrategy.Default and making it harmful for angular ecosystem?(with signals there will be 2 ways to run change detection in O(n), using signals or ChangeDetectionStrategy.Default(which is simpler to do than signals), and 1 way to run change detection in ~O(1) - using rxjs and async pipe)

Collapse
 
elpddev profile image
Eyal Lapid • Edited

I've followed the link. Great resource, thank you!

I was wondering, why say O(n) of all signals of the all application. Reading the code, it seems:

  1. the consumer register globally to notify it wanting signal registration,
  2. Then the consumer activates the computed callback function,
  3. the signals look for in that global registration the registered consumer and subscribe that consumer only
  4. when the callback is done, the consumer de-register from the global service to stop other signals activation later in other logics to subscribe it to them.

It seems that the consumer is collecting only the signals detected while executing the callback.

github.com/angular/angular/pull/49...

  signal(): T {
    producerAccessed(this);
    return this.value;
  }
Enter fullscreen mode Exit fullscreen mode

github.com/alxhub/angular/blob/119...

export function producerAccessed(producer: Producer): void {
  if (activeConsumer === null) {
    return;
  }

  // Either create or update the dependency `Edge` in both directions.
  let edge = activeConsumer.producers.get(producer.id);
Enter fullscreen mode Exit fullscreen mode

github.com/alxhub/angular/blob/119...

  private recomputeValue(): void {
   // ...

      const prevConsumer = setActiveConsumer(this);
    let newValue: T;
    try {
      newValue = this.computation();
    } catch (err) {
      newValue = ERRORED;
      this.error = err;
    } finally {
      setActiveConsumer(prevConsumer);
    }
Enter fullscreen mode Exit fullscreen mode

github.com/alxhub/angular/blob/119...

export function setActiveConsumer(consumer: Consumer|null): Consumer|null {
  const prevConsumer = activeConsumer;
  activeConsumer = consumer;
  return prevConsumer;
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ivanivanyuk1993 profile image
Ivan Ivanyuk

Oh, thanks, now I understand. We know for sure who is current consumer(because we track it in activeConsumer) and current producer(we can track it in code), so we have all the information needed to create dependency graph which will run atomically in O(1)

Collapse
 
goetzrobin profile image
Robin Goetz

Looks like I am playing catch-up here :D Thank you @elpddev! I had the same question and even better that you pointed to the direct lines in the code that outline the process!

I want to (again) point to @michael_hladky 's amazing talk at the NG - BE conference where he explains this exact mechanism! You can check it out here.

He also shows how we can leverage this built in reactivity of signals for fine grained updates to the DOM, not only on view level, but (with the help of structural directives) even on binding level!

Even better, he compares this mechanism with the RxJs & async pipe approach you are mentioning!

Please check it out and let me know if it addresses the concerns you mention in your comment!

Thread Thread
 
elpddev profile image
Eyal Lapid

Great video! He does goes into the details of the mechanism. The pipes design were very informative. It has a been a while that I actually dealt with Angular projects code.

It reminded me I encountered this kind of pattern years ago doing a project in Meteor in 2017 I think.

docs.meteor.com/api/tracker.html#T...

Tracker.autorun(() => {
  const oldest = _.max(Monkeys.find().fetch(), (monkey) => {
    return monkey.age;
  });

  if (oldest) {
    Session.set('oldest', oldest.name);
  }
});
Enter fullscreen mode Exit fullscreen mode

Going back to it looking at the code it seems the same pattern.

github.com/meteor/meteor/blob/77df...

  _compute() {
    this.invalidated = false;

    var previousInCompute = inCompute;
    inCompute = true;
    try {
      Tracker.withComputation(this, () => {
        withNoYieldsAllowed(this._func)(this);
      });
    } finally {
      inCompute = previousInCompute;
    }
  }
Enter fullscreen mode Exit fullscreen mode

Funny, it seems to be still alive somewhat. They even have a recent PR merge it seems for partial async support.

github.com/meteor/meteor/pull/12294

Collapse
 
santoshyadavdev profile image
Santosh Yadav

Great article, looking forward to see more articles from you.

Collapse
 
goetzrobin profile image
Robin Goetz

Can’t express how much this means to me Santosh! You and Lars’ community is the reason I started writing in the first place! Incredibly grateful for your positive impact on the Angular community and excited to be part of it!

All I can say is: Dankeschön mein Freund!

Collapse
 
santoshyadavdev profile image
Santosh Yadav

Danke Robin, I am happy to see your growth. Happy that we are able to build a great community, pass it forward brother, help more developers in their journey.

Collapse
 
andriyonoshko profile image
Andriy

This is a great article that was worth every minute spent reading it. Thank you for providing an in-depth analysis of all sides of the current state of change detection, as well as a detailed description of the Signal concept and mechanism.

Collapse
 
goetzrobin profile image
Robin Goetz

Thank you! I appreciate you taking the time reading it and am glad that you liked it!

Collapse
 
cyberdyme profile image
cyberdyme

If you take an existing application upgrade to version 16 or later when they are out. What work will be required to get the application working without zones and signals; are we talking significant changes or not or will they co-exist while you change the application component by component.

Collapse
 
goetzrobin profile image
Robin Goetz • Edited

Very good question! There will definitely be a way for signal and zone based change detection to co exist in the same application.

@eneajaho shared a tweet about a pull request introducing scoped zones, which means that we would be able to decide the way we want to run change detection on component level.
The tweet is here:

The conversation in the comments is also really interesting!

Let me know if that helps!

Collapse
 
fjorlin profile image
Marvin Tobisch

Amazing summary that really helped me understand how Angular Signals will work under the hood.

I guess what I'm left wondering about is the end goal the Angular team is chasing with this. By all accounts, it seems to be replacing Zone.js. But while the performance-benefits of Signals are undeniable, using them does effectively add boilerplate code (at the very least having to explicitly create a signal, then use .set() to change its value) compared to the magic that was Zone.js.

So in the end, Angular will have Signals that have a bit of learning attached to them and RxJS that has a lot more learning attached to it. Compared to before, where you had RxJS, but at least all other change detection happened automagically.

Not saying I disagree with the elegance that is Signals, but I doubt this change will do much to attract new devs to Angular that already found it too much of a bother to learn.

Collapse
 
goetzrobin profile image
Robin Goetz

Replacing zone based change detection is absolutely one of the main goals of this effort. Performance is one side of the coin, but the other side is that there are so many ways to make easy mistakes with the zone, especially if you are unfamiliar with how the "magic" actually works. With signals there is one simple mode to update the view: If you change the signal, the view will update.

So I would almost argue that the additional boilerplate is a feature, not a bug. Which values trigger a view update is now explicitly stated, directly in the code. Also, since we exactly know which values' changes trigger a view update, all the gotcha's that come with ZoneJs will disappear. Ultimately, developers will be able to care much less about how the framework detects changes and be able to fall back on one simple model: If its a signal, a change to its value will update the view.

Also, signals are not bound to just be used in a component. There is a great article by @ryansolid that lays out the benefits of using signals.

The example of how easy it is in SolidJs to turn local state into global state when using signals applies just as much to Angular. It does not matter if a signal is declared in a component or a service that is injected by Angular's incredibly dependency injection mechanism, it will work the same way.

To your point with RxJs. I agree that RxJs has a big learning curve and is something a lot of developers new to Angular struggle with. However, I believe that signals will actually help new Angular developers ease into RxJs. Signals will introduce new developers to reactivity and declarative code. They also solve a lot of issues that we currently (ab)use RxJs for better and easier. I am thinking of the example in the article of trying to declaratively derive state from 2 inputs. Ultimately, I think we will end up with an ecosystem that will still be integrated with RxJs, but this integration will use RxJs much better and only for the thing it is really good at: Describing the complete asynchronous behavior of each feature in its declaration.

I think that is definitely a valid feeling and I cannot say that signals alone will attract new Angular developers. I do want to point out that there is a lot more improvements coming to Angular and that the introduction of standalone components (making modules optional) was a huge improvement already. I think the most important thing to note for me here is that the Angular team seems to be very much aware of the initial learning curve of the framework and is working to make it easier for new developers to get started.

Finally, I think with the trajectory the framework is on it will also become "more worth it." A lot of the current issues with the framework are actively being worked on. Also, there are amazing community projects such as AnalogJS or Angular THREE that continue to make Angular even more powerful.

Ultimately, only time will tell how much signals will impact the popularity of the framework. However, as someone already working with Angular I couldn't be more excited. This will be an amazing new tool provided to all of us and I cannot wait to see what people will build with it!

Collapse
 
fjorlin profile image
Marvin Tobisch

Thank you for your detailed and thoughtful response!

Regarding the first point, it is true that Signals are going to make Angular more "transparent" to work with. As an Angular developer that only later learned React, this is something that I found strangely appealing about explicitly having to call setState/setVar as well. You are in direct control of how often the view updates and performance seemed at least implicitly much more efficient because of it. Compare that to Angular where just running console.log() in ngDoCheck() to see how often change detection runs for no reason can give you shivers.

But from what I understand about Signals, there are some new pitfalls that devs might have to learn.

Here is one example from Vue I found interesting. In order for Signals to be picked up as dependencies by a consumer, they have to be read at least once on the inital render/execution of the consumer. And if they're not (due to if/switch expressions, also async constructs (?)), the consumers won't update when the Signal changes. It won't be a problem if everything in a consumer is also strictly using Signals, but if you mix and match with normal variables, you might run into confusing problems. Or am I getting this wrong?

Additionally, and this is more of a personal preference, but I really liked the explicit .subscribe() call that you would for example use with RxJs observables. With Signals, from what I gather, the only way to "subscribe" to changes is to use "effect()". And the only way that effect runs is if you explicitly use the Signal inside it to register it as a dependency. But what if I don't care about the new value, but only care that it has changed? I would have to artificially read the Signal, perhaps at the start of the effect to register it as a dependency, do the actual logic afterwards. Not a dealbreaker I guess, but it doesn't seem elegant.

As someone who only learnt of Signals yesterday, something that is becoming apparent even to me however, is that a lot of the major frontend frameworks are moving closer together with this. Vue has had a form of Signals for a long time, React's Hooks also very similar, then there's obvious SolidJs and now Angular, all using the same reactive concept with very similar syntax as well. Feels like that is a good thing.

Thread Thread
 
goetzrobin profile image
Robin Goetz

I agree! There will definitely be new pitfalls and edge cases that we all run into. I think mixing reactive and non reactive variables in a reactive context is one of those things. Also, your point of subscribing to changes without needing the value is definitely valid. I know that SolidJs support an on function that allows you to explicitly declare dependencies, maybe that is something the Angular team will add too.

I would need to double check the Github discussion to see if this use case was already mentioned. If not, I would invite you to definitely bring this up and see what the Angular teams thinks about this! I would be excited to see their response!

Agreed! If you watched the stream with Ryan, Alex, and Pawel you could see their excitement for this "new" pattern that seems to address one of the core issues all frontend frameworks are dealing with: Keeping application state in sync with the user interface.

Thread Thread
 
elpddev profile image
Eyal Lapid • Edited

I was also thinking how signal architecture behave with conditional statements. I will be glad if someone could clarify this and how signals actually work. How the subscription mechanism works.

If I understand correctly, the only way a consumer to subscribe to a producer without doing "subscribe" explicitly like in Rxjs, is if they communicate in other ways:

  1. Through an implicit injection like "this" which wont work on anonymous functions
  2. Or through a global singleton context/service like an imported javascript module.

So the consumer has to do a subscription phase:

  1. It has to register its intention on subscribing with the global service,
  2. activating the callback,
  3. the signals primitives in the callback that are activated pick up the consumers from the global service and notify them to subscribe to them.
  4. The callback ends and the consumer de-register itself from the global service for wanting to be notified of new primitive activation.

I this is wrong, I will be happy to be corrected and learn how signals works in real.

But if this is right, then the subscribing is done only on the primitive that were actually had been activated. If those primitives where in a conditional statement (ex if/else) that was not entered, if that primitive at a later time change its value, the consumer will never know about it.

Is there an example of if/else inside a "compute" signal that use other signals inside the if/else? Or loops that exist conditionally?

This also is true for async/await as the callback must be synchronize, otherwise the registration phase must also be asynchronized, which means it will pick signals primitives existance notifications from other logic in the app that run in the meantime which the consumer did not intent to register to, all because they use the same global service to register/notify of existence.

Signals registration if I understand all this is rely on the fact that javascript is single threaded and the callback in sync so no other parties can notify the global service while the callback is running.

So I am not sure signals could be used in any other way then a simple synchronize computations that do not involve conditions also. Ex:

const ferengi_happiness = compute(() => products_sells * price);
Enter fullscreen mode Exit fullscreen mode

Is this correct? Or the mechanism is totally different?

** edit 1 **

Regarding the if/else conditions block, I've re-read @fjorlin comment. If I understand, so if the desire is to be reactive on the logic inside the "if" statement, than the condition of the "if" statement needs to be reactive also so the variables participating in the conditions needs to be signals also. Then when they change the computation is reactivated, the if section is entered, and the signals within it are picked up.

Thread Thread
 
goetzrobin profile image
Robin Goetz

You are correct on the if/else conditions. For signals to be conditionally added and removed as triggers of the execution context, the condition must be reactive (one might call it signal driven) also. Only then does the automatic subscribe/unsubscribe logic that drives signals under the hood kick in.

This talk explains in more detail how the registration process works. It lays out the overall concept and gives some good examples, which includes computed and effect. Let me know if this clarifies some of your questions or if you'd want me to go into more details of the subscribe/unsubscribe mechanism!

Collapse
 
joelbonetr profile image
JoelBonetR 🥇

Top post!

Do Angular already implements signals on the stable version?

Didn't used it since v6, it may be worth to take a try at this 😁

Collapse
 
goetzrobin profile image
Robin Goetz

Signals will be officially part of the v16 release later this year.

There is a first release candidate that includes an early prototype for developers to get started: github.com/angular/angular/release...

There’s also some great resources by the community. My go to is this stackblitz by Enea Jahollari: stackblitz.com/edit/angular-ednkcj...

Hope this helps!

Collapse
 
joelbonetr profile image
JoelBonetR 🥇

Thank you very much Robin!

Collapse
 
spock123 profile image
Lars Rye Jeppesen

You can try initial stuff in the 16RC0

Collapse
 
aminerhanemi_79 profile image
amine rhanemi

Thank you for the great article.
I was wandering how does Push/Pull Algorithm works when a consumer depends on two producer. Similar to the first name / last name example.
Why does the counter is equal to 2 and not 3? what it the equivalent of debounceTime(0) in signals ?

Collapse
 
goetzrobin profile image
Robin Goetz • Edited

Thank you! Those are some great questions. The reason the counter is equal to 2 and not 3 lies in this part of the official documentation provided by the Angular team:

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.

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. This allows the change notification to reach all affected nodes in the graph without the possibility of observing intermediate or glitchy states.

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.

In short, Angular will know that both first and last name changed at the same time before it re-executes the effect.

combineLatest on the other hand emits once for every input observable that changes. Even if they are changed at the same time. To avoid that we use debounceTime(0), to just wait long enough to skip the first emission and also push both new values at the same time.

Therefore, there is no equivalent of debounceTime with signals. It is baked into the mechanism.

Collapse
 
aminerhanemi_79 profile image
amine rhanemi

Very clear, thank you for the explanation.

Collapse
 
davdev82 profile image
Dhaval Vaghani

This is really awesome article. One thing I am not sure is if the change happens at a parent component? Will the child component need refreshing ? I am tempted to think no. With this fine grain reactivity with signals we know exactly which components template needs refreshing and therefore there is no need to refresh the templates child components. If indeed the child components signal have also changed, then its effect will get scheduled and therefore trigger a localized Dom update of the components template only (excluding the child components)

What are your thought ?

Collapse
 
goetzrobin profile image
Robin Goetz

Thank you! I am glad you enjoyed it!

We are still waiting on signal based change detection to be implemented, but the way I understood the Angular team members on the live stream with Ryan Carniato is in line with what you are saying here.

Change detection will not be too down anymore, which means that parent and child will be updated independently. Actually, the way I understood it, signals even enable updates on the binding level, which means that even within a template only the parts that changed would be updated.

We are still waiting for the RFC and initial PRs that enable this signal based change detection, but I am excited for when it is released and the amazing performance benefits that will come with it.

I hope that answers your question!

Collapse
 
timonkrebs profile image
timonkrebs • Edited

Great article. Thanks for putting this all together. I think one important thing is missing. Its the benefits for understanding the change detection since the reactive graph can be visualized. And likewise can be reasoned about this way: twitter.com/thetarnav/status/16251...

Collapse
 
goetzrobin profile image
Robin Goetz

Thank you so much for your comment! I will try to add a paragraph about this! I saw that Pawel from the Angular team tweeted about this too: twitter.com/pkozlowski_os/status/1...

Seems like they’re working on a tool similar to the one you mentioned!

This would be an absolute game changer for all of us!

Collapse
 
mandrewdarts profile image
M. Andrew Darts

This is awesome! Thank you for bringing all this together and doing a quick deep dive 🤘

Collapse
 
goetzrobin profile image
Robin Goetz

Thank you!

Collapse
 
thavarajan profile image
Thavarajan

Wow what an explanation, in particular i didn't even hear the signals, but the way you explain is awesome, Great read

Collapse
 
goetzrobin profile image
Robin Goetz

I am glad you enjoyed the article! Exciting times for Angular so I’m glad that my article helps more people to understand how big a deal this is!

Collapse
 
roverbober profile image
roverbober

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.

That's not accurate (as far as I know). The component will not be rendered immediately after the 'firstName' is updated. The 'firstName' and 'lastName' changes occur within the same event loop task. Therefore, Angular waits until all calculations in the task are complete before rendering the component.

Collapse
 
goetzrobin profile image
Robin Goetz

You’re right! Here’s a little more on it: itnext.io/microtask-queue-rxjs-ang...

I shouldn’t say rendering. The tap operator executes and increases the value, but rendering is “debounced” until the microtask queue is empty… So rendering might be the wrong example, but if you used the inputs to e.g trigger a network request you’d send that request twice. Thanks for catching that! I’ll try to update the article!

Collapse
 
rakesh24386 profile image
Rakesh Kumar

Thanks for this article. This is really awesome.

Collapse
 
goetzrobin profile image
Robin Goetz

I am happy that you find it helpful!

Collapse
 
yksolanki9 profile image
Yash Solanki

Amazing article! Super easy to understand. Thanks @goetzrobin for writing this!

Collapse
 
goetzrobin profile image
Robin Goetz

Thank you very much! I am glad you enjoyed the article! Let me know if there’s other topics in Angular you’d like me to explore!

Collapse
 
aboudard profile image
Alain Boudard

Well mission success I must say.
You did an amazing job by browsing all the aspects of this topic ! Thanks a lot 👌

Collapse
 
goetzrobin profile image
Robin Goetz

Thank you! I am glad you find the article helpful!