Skip to content

An Introduction to Signals

Signals are one of the new features in Angular, and it is pretty safe to say they are the new feature.

It might be hard to see why… or even understand what exactly it is they allow us to do.

For example, here is a basic interpolation that we have already covered:

import { Component } from '@angular/core';
@Component({
selector: 'app-home',
template: `<p>Hi, {{ name }}</p>`,
})
export class HomeComponent {
name = 'Josh';
}

Now here is what it will look like with signals:

import { Component, signal } from '@angular/core';
@Component({
selector: 'app-home',
template: `<p>Hi, {{ name() }}</p>`,
})
export class HomeComponent {
name = signal('Josh');
}

It’s basically exactly the same… except we have to wrap our Josh value with this signal function that is imported from @angular/core, and in the template we also have to use this funny syntax to access the value:

{{ name() }}

When we create our name:

name = signal('Josh');

It will become whatever this signal function we are calling returns. The signal function will actually return us something called a WritableSignal which is a type of signal (specifically, one you are allowed to update).

So, basically, the signal function will return us a signal. That means name is a signal.

To access the value of a signal we call it as a function — that is what we are doing in the template:

{{ name() }}

If we did this instead:

{{ name }}

You would see some nonsense like this in the template:

Hi, function signalFn() { producerAccessed(node); return node.value; }

So it seems that signals do basically the same thing as declaring a class member and using an interpolation, except that it requires extra syntax and comes with some extra confusing nonsense.

Signals and Change Detection

Ok, so given how excited everyone is about these things, it’s safe to say there is some benefit to them.

Perhaps the key thing is how they tie into change detection. This is a topic we are going to cover in a lot more depth in a few lessons time, but what is important to understand for now is that Angular needs to know when something has changed in order to update the view and actually display the change to the user.

For example:

import { Component } from '@angular/core';
@Component({
selector: 'app-home',
template: `<p>Hi, {{ name }}</p>`,
})
export class HomeComponent {
name = 'Josh';
}

If name changes from Josh to Kathy for any reason, Angular needs to know so that it can update the DOM appropriately. How does Angular keep track of when things change? To put it simply, it use a mechanism (Zone.js) that will notify it when anything possibly could have happened to cause a change. Things that could potentially cause a change are things like setTimeout or setInterval or a promise resolving, or an HTTP request completing, and so on.

Angular gets notified that there could have been a change, and then it checks every component to see if anything needs to be updated.

This isn’t as bad as it sounds — Angular can actually do this quite fast and there are ways to optimise it on the developers end. But, there is certainly room for improvement.

That is where signals come in. It flips this whole problem on its head. Instead of Angular checking whenever anything could have possibly changed, it becomes the role of the signal to tell Angular when it has changed.

If we use signals for our values that change, then it allows Angular to do a lot less change detection work. So, if we do something like this:

name = signal('Josh');
updateName(name: string) {
this.name.set(name);
}

We are calling set on our signal which will update its value, but it can also notify Angular that it has changed. It’s also not just Angular that it will notify about the change, we can make use of this mechanism as well (more on that later).

It’s important to note that these change detection changes have not actually been implemented in Angular yet, they will be coming soon in an update. However, as we go through this course we will basically just be pretending that these changes are already in place. Using signals with the current/default change detection strategy will still work anyway, and when the change detection updates do land we will just get all of the benefits for free without needing to change anything.

Basic Signals API

We will be talking about signals a lot in this course. For now, let’s just get a basic understanding of the API.

As we have seen, we can create a signal:

name = signal('Josh');

and access its value as a function:

name()

We can also update a signal’s value with set:

this.name.set(name);

We can also update a signal’s value with the update method. The benefit of this approach is that it gives us a reference to the existing value. This becomes useful when dealing with objects in a signal. For example:

preferences = signal({
fast: true,
comfortable: true,
expensive: false,
});
toggleComfort() {
this.preferences.update((preferences) => ({
...preferences,
comfortable: !preferences.comfortable,
}));
}

Our signal now contains an object. In the toggleComfort method we want flip the boolean for the comfortable property. By using the update method we spread the existing preferences into a new object so that we can keep all of its existing values, and then we supply a new value for the comfortable property which will be the opposite of whatever it currently is.

The end result will be:

{
fast: true,
comfortable: false,
expensive: false,
}

We can also create computed signals:

preferences = signal({
fast: true,
comfortable: true,
expensive: false,
});
comfortable = computed(() => this.preferences().comfortable);

Now, comfortable will be the value of whatever comfortable in the preferences signal is. The important part is that this computed will be notified whenever any signals it depends on change and it will re-calculate its value. That means that whenever comfortable in the preferences signal updates, the value of our comfortable signal will update as well.

A computed signal is still a signal, and it behaves in the same way that the signals we create with signal() do, except that it is not writeable. You can not manually update the value of a computed signal by calling set or update. It will only update when the signals it depends upon change.

All we are doing is extracting a value from another signal, but we can create much more complex computations. We could combine multiple signals together:

name = signal('Josh');
preferences = signal({
fast: true,
comfortable: true,
expensive: false,
});
comfortable = computed(() => this.preferences().comfortable);
message = computed(() => {
const message = `${this.name()} likes it ${
this.comfortable() ? 'comfortable' : 'uncomfortable'
}`;
return message;
});

Now the message signal depends on both the name signal and the comfortable signal. If either of these signals are updated, the message will update as well.

Originally, the message will be:

Josh likes it comfortable

But then if the name signal has its value updated to Dave the message will automatically become:

Dave likes it comfortable

Then if the preferences signal is updated such that comfortable is false, the message will become:

Dave likes it uncomfortable

Then we have the idea of an effect, which is actually a lot like the computed in that it will run any time any of the signals it depends upon change. However, an effect is for running arbitrary code — not calculating the value for a new signal:

constructor() {
effect(() => {
console.log(this.comfortable());
console.log(this.name());
});
}

This effect will run once initially, and then again any time either the comfortable or name signals update. This effect will log both values any time either of them updates. If you only wanted the value that was changed to be logged you would have to have them in two separate effects:

constructor() {
effect(() => {
console.log(this.comfortable());
});
effect(() => {
console.log(this.name());
});
}

We will actually use this quite a bit for practical purposes later. A couple of examples are using an effect to trigger saving data to storage, and using an effect to trigger a navigation when something specific happens.

One other important aspect about signals is that they only trigger updates if their value changes. That means that if the value was previously Josh and then it is set again with Josh nothing that depends on it will be notified (the effect would not run, for example).

Summary

There is a lot more to know about signals and we are going to be talking about them a lot more as we go. Perhaps the biggest impact of signals from a developer perspective is how they relate to reactivity and interact with RxJS (we have an entire module dedicated to that coming up later).

Recap

    Which of these is NOT a benefit of signals

    How do you access the value of a signal in the template?

    Which of these is a way to create a signal?

    Which of these methods is NOT used to change a signal’s value?

    Which of these best describe the role of a computed signal?

    Which of these best describe the role of a signal effect?