Angular Signals were originally introduced in Angular 17, and they are really powerful.
The whole goal of signals is to give developers a new easy-to-use reactive primitive that can be used to build applications in reactive style.
Signals have an easy-to-understand API for reporting data changes to the framework, allowing the framework to optimize change detection and re-rendering in a way that so far was just not possible.
With Signals, Angular will be able to determine exactly what parts of the page need to be updated and update only those and nothing more.
This comes in contrast with what currently happens with for example default change detection, where Angular has to check all the components on the page, even if the data that they consume didn't change.
Signals are all about enabling very fine-grained updates to the DOM that are just not possible with the current change detection systems that we have available.
Signals are all about increasing the runtime performance of your application, by getting rid of Zone.js.
Even without the runtime performance benefits, signals are just a great way to build applications in reactive style in general.
Signals are also way easier to use and understand than RxJS when it comes to propagating data changes throughout an application.
Signals are a game changer, and they take reactivity in Angular to a whole new level!
In this guide, we will explore the Signals API in detail, explain how signals work in general, as well as talk about the most common pitfalls that you should watch out for.
Note: Signals are not yet plugged into the Angular change detection system.At this point, even though most of the Signals API is already out of developer preview, there aren't yet any signal-based components.
This could be available around Angular 17.2, to be confirmed.
But that doesn't prevent us from already starting to learn right now about signals and their advantages, in preparation for what is to come very soon.
Table of Contents
This post covers the following topics:
- What are Signals?
- How to read the value of a signal?
- How to modify the value of a signal?
- The update() Signal API
- What is the main advantage of using signals instead of primitive values?
- The computed() Signal API
- How do we subscribe to a signal?
- Can we read the value of a signal from a computed signal without creating a dependency?
- What is the major pitfall to look out for when creating computed signals?
- Are signal dependencies determined based only on the initial call to the compute function?
- Signals with array and object values
- Overriding the signal equality check
- Detecting signal changes with the effect() API
- What is the relation between Signals and change detection?
- How to fix the error NG0600: Writing to signals is not allowed
- How to set signals from inside an effect, if needed
- The default effect clean-up mechanism
- How to clean up effects manually
- Performing cleanup operations when an effect is destroyed
- Read-only signals
- Using a signal in multiple components
- How to create signal-based reactive data services
- Signals and OnPush components
- Can I create signals outside of components/stores/services?
- How do Signals compare to RxJs?
- Summary
Note: If you are looking to learn about other Angular 17 features besides signals, check out the following posts:
- Angular @if: Complete Guide
- Angular @for: Complete Guide
- Angular @switch: Complete Guide
- Angular @defer: Complete Guide
And if you are looking to learn about Signal inputs, check out:
Angular Signal Inputs: Complete Guide
What are Signals?
Simply put, a signal is a reactive primitive that represents a value and that allows us to change that same value in a controlled way and track its changes over time.
Signals are not a new concept exclusive to Angular. They have been around for years in other frameworks, sometimes under different names.
To better understand signals, let's start with a simple example that does not use signals yet, and then re-write the same example using the Signals API.
To start, let's say that we have a simple Angular component with a counter variable:
@Component(
selector: "app",
template: `
<h1>Current value of the counter {{counter}}</h1>
<button (click)="increment()">Increment</button>
`)
export class AppComponent {
counter: number = 0;
increment() {
this.counter++;
}
}
As you can see, this is a very simple component.
It just displays a counter value, and it has a button to increment the counter. This component uses the Angular default change detection mechanism.
This means that the {{counter}}
expression as well as any other expressions on the page are being checked for changes after each event, such as a click on the increment button.
As you can imagine, that could be a potentially ineffective way to try to detect what needs to be updated on the page.
In the case of our component, this approach is actually even necessary, because we are using a mutable plain Javascript member variable like counter
to store our state.
So when an event occurs, just about anything on the page could have affected that data.
Furthermore, the click on the increment button could have easily triggered changes anywhere else on the page as well, and not just inside this component.
Imagine for example that we would call a shared service that would affect multiple parts of the page.
With default change detection, there is no way for Angular to know exactly what has changed on the page, so that is why we cannot make any assumptions about what happened, and we need to check everything!
Because we have no guarantees of what could or could not have changed, we need to scan the whole component tree and all the expressions on every component.
With default change detection, there is no other way.
But here is where signals come to the rescue!
Here is the same simple example, but this time re-written using the Signals API:
@Component(
selector: "app",
template: `
<h1>Current value of the counter {{counter()}}</h1>
<button (click)="increment()">Increment</button>
`)
export class AppComponent {
counter = signal(0);
constructor() {
console.log(`counter value: ${this.counter()}`)
}
increment() {
console.log(`Updating counter...`)
this.counter.set(this.counter() + 1);
}
}
As you can see, the signal-based version of our component doesn't look that much different.
The major difference is that we are now wrapping our counter value in a signal using the signal()
API, instead of just using a plain counter
Javascript member variable.
This signal represents the value of a counter, that starts at zero.
We can see that the signal acts like a container for the value that we want to keep track of.
How to read the value of a signal?
The signal wraps the value that it represents, but we can get that value at any time just by calling the signal as a function, without passing it any arguments.
Notice the code on the constructor of the signal-based version of our AppComponent:
constructor() {
console.log(`counter value: ${this.counter()}`)
}
As you can see, by calling counter()
, we get back the value wrapped in the signal, which in this case would resolve to zero (the initial value of the signal).
How to modify the value of a signal?
There are a couple of different ways to change the value of a signal.
In our case, we are using the set()
API in our implementation of the counter increment function:
increment() {
console.log(`Updating counter...`)
this.counter.set(this.counter() + 1);
}
We can use the set()
API to set any value that we need in the signal, as long as the value is of the same type as the initial value of the signal.
So in the case of our counter signal, we can only set numbers because the initial value of the signal was zero.
The update signal API
Besides the set()
API, we also have available the update()
API.
Let's use it to re-write our counter increment function:
increment() {
console.log(`Updating counter...`)
this.counter.update(counter => counter + 1);
}
As you can see, the update API takes a function that receives the current value of the signal as an input argument, and then it returns the new value of the signal.
Both versions of the increment method are equivalent and work equally well.
What is the main advantage of using signals instead of primitive values?
At this point, we can now see that a signal is just a lightweight wrapper or a container for a value.
So what is the advantage of using it then?
The main advantage is that we can get notified when the signal value changes, and then do something in response to the new signal value.
This is unlike the case when we used just a plain value for our counter.
With a plain value, there is no way for us to get notified when a value changes.
But with Signals? Easy!
And that's the whole point of using signals in the first place.
The computed()
Signal API
Signals can be created and derived from other signals. When a signal updates, all its dependent signals will then get updated automatically.
For example, let's say that we would like to have a derived counter on our component, that is the value of the counter multiplied by 10.
We can create a derived signal based on our counter signal, by using the
computed()
API:
@Component(
selector: "app",
template: `
<h3>Counter value {{counter()}}</h3>
<h3>10x counter: {{derivedCounter()}}</h3>
<button (click)="increment()">Increment</button>
`)
export class AppComponent {
counter = signal(0);
derivedCounter = computed(() => {
return this.counter() * 10;
})
increment() {
console.log(`Updating counter...`)
this.counter.set(this.counter() + 1);
}
}
The computed API works by taking in one or more source signals, and creating a new signal.
When the source signal changes (in our case the counter signal), the computed signal derivedCounter gets updated as well, instantly.
So when we click on the Increment button, the counter will have the initial value of 1 and the derivedCounter will be 10, then 2 and 20, 3 and 30, etc.
How do we subscribe to a signal?
Notice that the derivedCounter signal did not subscribe in any explicit way to the source counter signal.
The only thing that it did was call the source signal using counter()
inside its computed function.
But that's all that we needed to do to link the two signals together!
Now whenever the counter source signal has a new value, the derived signal also gets updated automatically.
It all sounds kind of magical, so let's break down what is going on:
- Whenever we are creating a computed signal, the function passed to
computed()
is going to get called at least once, to determine the initial value of the derived signal - Angular is keeping track as the compute function is being called and takes note when other signals that it knows are being used.
- Angular will notice that when the value of derivedCounter is being calculated, the signal getter function
counter()
is called. - So Angular now knows that there is a dependency between the two signals, so whenever the counter signal gets set with a new value, the derived
derivedCounter
will also be updated
As you can see, this way the framework has all the information about what signals depend on other signals.
Angular knows the whole signal dependency tree and knows how changing the value of one signal will affect all the other signals of the application.
Can we read the value of a signal from a computed signal without creating a dependency?
There could be certain advanced scenarios where we want to read the value of a signal from another computed signal, but without creating any dependency between both signals.
This should be rarely needed, but if you ever run into a situation where you need to do this, here is how you do it:
@Component(
selector: "app",
template: `
<h3>Counter value {{counter()}}</h3>
<h3>10x counter: {{derivedCounter()}}</h3>
`)
export class AppComponent {
counter = signal(0);
derivedCounter = computed(() => {
return untracked(this.counter) * 10;
})
}
By using the untracked API, we can access the value of the counter signal without creating a dependency between the counter and the derivedCounter signal.
Notice that this untracked feature is an advanced feature that should be rarely needed, if ever.
If you find yourself using this feature very often, something is not right.
What is the major pitfall to look out for when creating computed signals?
For everything to work smoothly, we need to make sure that we compute our derived signals in a certain way.
Remember, Angular will only consider that one signal depends on another if it notices that a signal is needed to calculate the value of another signal.
This means that we need to be careful with introducing conditional logic inside the computed function.
Here is an example of how things could easily go wrong:
@Component(
selector: "app",
template: `
<h3>Counter value {{counter()}}</h3>
<h3>Derived counter: {{derivedCounter()}}</h3>
<button (click)="increment()">Increment</button>
<button (click)="multiplier = 10">
Set multiplier to 10
</button>
`)
export class AppComponent {
counter = signal(0);
multiplier: number = 0;
derivedCounter = computed(() => {
if (this.multiplier < 10) {
return 0
}
else {
return this.counter() * this.multiplier;
}
})
increment() {
console.log(`Updating counter...`)
this.counter.set(this.counter() + 1);
}
}
As you can see in this example, there is some conditional logic in the compute function.
We are calling the counter()
source signal like before, but only if a certain condition is met.
The goal of this logic would be to set the value of the multiplier dynamically, via some user action like clicking on the "Set multiplier to 10" button.
But this logic does not work as expected!
If you run it, the counter itself will still be incremented.
But the expression {{derivedCounter()}}
will always be evaluated to zero, both before and after the "Set multiplier to 10" gets clicked.
The problem is that when we calculate the derived value, no calls to counter()
are made initially.
The call to counter()
is made inside an else
branch, that does not get run initially.
So Angular does not know that there was a dependency between the counter signal and the derivedCounter signal.
Those are considered by Angular as two completely independent signals, without any connection.
This is why when we update the value of the counter, derivedCounter does not get updated.
This means that we need to be somewhat careful when defining computed signals.
If a derived signal depends on a source signal, we need to make sure we call the source signal every time that the compute function gets called.
Otherwise, the dependency between the two signals will be broken.
This does not mean that we can't have any conditional branching at all inside a compute function.
For example, the following code would work correctly:
@Component(
selector: "app",
template: `
<h3>Counter value {{counter()}}</h3>
<h3>Derived counter: {{derivedCounter()}}</h3>
<button (click)="increment()">Increment</button>
<button (click)="multiplier = 10">
Set multiplier to 10
</button>
`)
export class AppComponent {
counter = signal(0);
multiplier: number = 0;
derivedCounter = computed(() => {
if (this.counter() == 0) {
return 0
}
else {
return this.counter() * this.multiplier;
}
})
increment() {
console.log(`Updating counter...`)
this.counter.set(this.counter() + 1);
}
}
With this version of the code, our compute function is now calling this.counter()
in every call.
So Angular can now identify the dependency between the two signals, and the code works as expected.
This means that after clicking the "Set multiplier to 10" button the first time, the multiplier will be applied correctly.
Are signal dependencies determined based only on the initial call to the computed function?
No, instead the dependencies of a derived signal are identified dynamically based on its last calculated value.
So with every new call of the computed function, the source signals of the computed signal are identified again.
This means that a signal's dependencies are dynamic, they are not fixed for the lifetime of the signal.
Again, this means that we need to be careful with any conditional logic that we create when defining a derived signal.
Signals with array and object values
So far, we have been showing examples of a signal with a primitive type, like a number.
But what would happen if we define a signal whose value is an array or an object?
Arrays and objects work mostly the same way as primitive types, but there are just a couple of things to watch out for.
For example, let's take the case of a signal whose value is an array, and another signal whose value is an object:
@Component(
selector: "app",
template: `
<h3>List value: {{list()}}</h3>
<h3>Object title: {{object().title}}</h3>
`)
export class AppComponent {
list = signal([
"Hello",
"World"
]);
object = signal({
id: 1,
title: "Angular For Beginners"
});
constructor() {
this.list().push("Again");
this.object().title = "overwriting title";
}
}
There is nothing special about these signals, in the sense that we can access their values as usual just by calling the signal as a function.
But the key thing to notice is that unlike a primitive value, nothing prevents us from mutating the content of the array directly by calling push on it, or from mutating an object property.
So in this example, the output generated to the screen would be:
- "Hello", "World", "Again" in the case of the list
- "overwriting title" in the case of the object title
This is of course not how Signals are meant to be used!
Instead, we want to update the value of a signal by always using the set()
and update()
APIs.
By doing so, we give a change to all the derived signals for updating themselves and have those changes reflected on the page.
By directly accessing the signal value and mutating its value directly, we are bypassing the whole signal system and potentially causing all sorts of bugs.
It is worth keeping this in mind, we should avoid mutating signal values directly, and instead always use the Signals API.
This is worth mentioning because there is currently no protective mechanism in the Signals API for preventing this misuse, like preventively freezing the array or the object value.
Overriding the signal equality check
Another thing worth mentioning regarding array or object signals is that the default equality check is "===".
This equality check is important because a signal will only emit a new value if the new value that we are trying to emit is different then the previous value.
If the value that we are trying to emit is considered to be the same as the previous value, then Angular will not emit the new signal value.
This is a performance optimization that potentially prevents unnecessary re-rendering of the page, in case we are systematically emitting the same value.
The default behavior however is based on "===" referential equality, which does not allow us to identify arrays or objects that are functionally identical.
If we want to do that, we need to override the signal equality function and provide our implementation.
To understand this, let's start with a simple example where we are still using the default equality check for a signal object.
Then we create a derived signal from it:
@Component(
selector: "app",
template: `
<h3>Object title: {{title()}}</h3>
<button (click)="updateObject()">Update</button>
`)
export class AppComponent {
object = signal({
id: 1,
title: "Angular For Beginners"
});
title = computed(() => {
console.log(`Calling computed() function...`)
const course = this.object();
return course.title;
})
updateObject() {
// We are setting the signal with the exact same
// object to see if the derived title signal will
// be recalculated or not
this.object.set({
id: 1,
title: "Angular For Beginners"
});
}
}
In this example, if we click on the Update button multiple times, we are going to get multiple logging lines in the console:
Calling computed() function...
Calling computed() function...
Calling computed() function...
Calling computed() function...
etc.
This is because the default "===" cannot detect that we are passing to the object signal a value that is functionally equivalent to the current value.
Because of this, the signal will consider that the two values are different, and so any computed signals that depend on the object signal will also get computed.
If we want to avoid this, we need to pass our equality function to the signal:
object = signal(
{
id: 1,
title: "Angular For Beginners",
},
{
equal: (a, b) => {
return a.id === b.id && a.title == b.title;
},
}
);
With this equality function in place, we are now doing a deep comparison of the object based on the values of its properties.
With this new equality function, the derived signal is only computed once no matter how many times we click on the Update button:
Calling computed() function...
It's worth mentioning that in most cases, we shouldn't provide this type of equality functions for our signals.
In practice, the default equality check works just fine, and using a custom equality check should rarely make any noticeable difference.
Writing this type of custom equality check could lead to maintainability issues and all sorts of weird bugs if for example we add a property to the object and forget to update the comparison function.
Equality functions are covered in this guide just for completeness in the rare case when you need them, but in general, for most use cases there is no need to use this feature.
Detecting signal changes with the effect() API
The use of the computed API shows us that one of the most interesting properties of signals is that we can somehow detect when they change.
After all, that is exactly what the computed()
API is doing, right?
It detects that a source signal has changed, and in response, it calculates the value of a derived signal.
But what if instead of calculating the new value of a dependent signal, we just want to detect that a value has changed for some other reason?
Imagine that you are in a situation where you need to detect that the value of a signal (or a set of signals) has changed to perform some sort of side effect, that does not modify other signals.
This could be for example:
- logging the value of a number of signals using a logging library
- exporting the value of a signal to localStorage or a cookie
- saving the value of a signal transparently to the database in the background
- etc.
All of these scenarios can be implemented using the effect()
API:
//The effect will be re-run whenever any
// of the signals that it uses changes value.
effect(() => {
// We just have to use the source signals
// somewhere inside this effect
const currentCount = this.counter();
const derivedCounter = this.derivedCounter();
console.log(`current values: ${currentCount}
${derivedCounter}`);
});
This effect will print out a logging statement to the console anytime that the counter or derivedCounter signals emit a new value.
Notice that this effect function will run at least once when the effect is declared.
This initial run allows us to determine the initial dependencies of the effect.
Just like in the case of the computed()
API, the signal dependencies of an effect are determined dynamically based on the last call to the effect function.
What is the relation between Signals and change detection?
I think you can see where this is going...
Signals allow us to easily track changes to application data.
Now imagine the following: let's say we put all our application data inside signals!
The first point to mention is that the code of the application wouldn't become much more complicated because of that.
The Signals API and the signal concept are pretty straightforward, so a codebase that uses signals everywhere would remain quite readable.
Still, that application code wouldn't be as simple to read as if it was just using plain Javascript member variables for the application data.
So what is the advantage then?
Why would we want to switch to a signal-based approach for handling our data?
The advantage is that with signals, we can easily detect when any part of the application data changes, and update any dependencies automatically.
Now imagine that the Angular change detection is plugged into the signals of your application, and that Angular knows in which components and expressions every signal is being used.
This would enable Angular to know exactly what data has changed in the application, and what components and expressions need to be updated in response to a new signal value.
There would no longer be the need to check the whole component tree, like in the case of default change detection!
If we give Angular the guarantee that all the application data goes inside a signal, Angular all of a sudden has all the necessary information needed for doing the most optimal change detection and re-rendering possible.
Angular would know how to update the page with the latest data in the most optimal way possible.
And that is the main performance benefit of using signals!
Just by wrapping our data in a signal, we enable Angular to give us the most optimal performance possible in terms of DOM updates.
And with this, you now have a pretty good view of how signals work and why they are useful.
The question is now, how to use them properly?
Before talking about some common patterns on how to use signals in an application, let's first quickly finish our coverage of the effect()
API.
How to fix the error NG0600: Writing to signals is not allowed in a computed
or an effect
by default.
By default, Angular does not allow a signal value to be changed from inside an effect function.
So for example, we can't do this:
@Component({...})
export class CounterComponent {
counter = signal(0);
constructor() {
effect(() => {
this.counter.set(1);
});
}
}
This code is not allowed by default, and for very good reasons.
In the particular case of this example, this would even create an infinite loop and break the whole application!
How to set signals from inside an effect, if needed
There are certain cases however where we still want to be able to update other signals from inside an effect.
To allow that we can pass the option allowSignalWrites
to effect
:
@Component({...})
export class CounterComponent {
count = signal(0);
constructor() {
effect(() => {
this.count.set(1);
},
{
allowSignalWrites: true
});
}
}
This option needs to be used very carefully though, and only in special situations.
In most cases, you shouldn't need this option. If you find yourself using this option systematically, revisit the design of your application because something is probably wrong.
The default effect clean-up mechanism
An effect is a function that gets called in response to a change in a signal's value.
And like any function, it can create references to other variables in the application, via a closure.
This means that just like any function, we need to watch out for the potential of creating accidental memory leaks when using effects.
To help cope with this, Angular will by default automatically clean up the effect function, depending on the context where it was used.
So for example, if you create the effect inside a component, Angular will clean up the effect when the component gets destroyed, and the same goes for directives, etc.
How to clean up effects manually
But sometimes, for some reason, you might want instead to clean up your effects manually.
This should only be necessary on rare occasions though.
If you are systematically cleaning up the effects manually everywhere in your application, something is likely not right.
But in case you need to, an effect()
can be manually destroyed by calling
destroy
on the EffectRef
instance that it returns when first created.
In those cases, you probably also want to disable the default cleanup behavior by using the manualCleanup
option:
@Component({...})
export class CounterComponent {
count = signal(0);
constructor() {
const effectRef = effect(() => {
console.log(`current value: ${this.count()}`);
},
{
manualCleanup: true
});
// we can manually destroy the effect
// at any time
effectRef.destroy();
}
}
The manualCleanup
flag disables the default cleanup mechanism, allowing us to have full control over when the effect gets destroyed.
The effectRef.destroy()
method will destroy the effect, removing it from any upcoming scheduled executions, and cleaning up any references to variables outside the scope of the effect function, potentially preventing memory leaks.
Performing cleanup operations when an effect is destroyed
Sometimes just removing the effect function from memory is not enough for a proper cleanup.
On some occasions, we might want to perform some sort of cleanup operation like closing a network connection or otherwise releasing some resources when an effect gets destroyed.
To support these use cases, we can pass to an effect a onCleanup
callback function:
@Component({...})
export class CounterComponent {
count = signal(0);
constructor() {
effect((onCleanup) => {
console.log(`current value: ${this.count()}`);
onCleanup(() => {
console.log("Perform cleanup action here");
});
});
}
}
This function will be called when the cleanup takes place.
Inside the onCleanup
function, we can do any cleanup we want, such for example:
- unsubscribing from an observable
- closing a network or database connection
- clearing setTimeout or setInterval
- etc.
Let's now explore a few more signal-related concepts, and then show some common patterns that you are likely to need when using signals in your application.
Read-only signals
We have already been using read-only signals already, even without noticing.
These are signals whose value can't be mutated. These are like the equivalent of const
in the JavaScript language.
Readonly signals can be accessed to read their value but can't be changed using the set or update methods. Read-only signals do not have any built-in mechanism that would prevent deep mutation of their value. - Angular repo
Read-only signals can be created from:
computed()
signal.asReadonly()
Let's try to change the value of a derived signal, just to see what happens:
@Component(
selector: "app",
template: `
<h3>Counter value {{counter()}}</h3>
<h3>Derived counter: {{derivedCounter()}}</h3>
`)
export class AppComponent {
counter = signal(0);
derivedCounter = computed(() => this.counter() * 10)
constructor() {
// this works as expected
this.counter.set(5);
// this throws a compilation error
this.derivedCounter.set(50);
}
}
As we can see, we can set new values for the counter signal, which is a normal writeable signal.
But we can't set values on the derivedCounter signal, as both the set()
and
update()
APIs are unavailable.
This means that derivedCounter is a read-only signal.
If you need to, you can easily derive a read-only signal from a writeable signal:
@Component(
selector: "app",
template: `
<h3>Counter value {{counter()}}</h3>
`)
export class AppComponent {
counter = signal(0);
constructor() {
const readOnlyCounter = this.counter.asReadonly();
// this throws a compilation error
readOnlyCounter.set(5);
}
}
Notice that the other way around is not possible: you don't have an API to create a writeable signal from a read-only signal.
To do that, you would have to create a new signal with the current value of the read-only signal.
Using a signal in multiple components
Let's now talk about some common patterns that you have available for using signals in your application.
If a signal is only used inside a component, then the best solution is to turn it into a member variable as we have been doing so far.
But what if the signal data is needed in several different places of the application?
Well, nothing prevents us from creating a signal and using it in multiple components.
When the signal changes, all the components that use the signal will be updated.
// main.ts
import { signal } from "@angular/core";
export const count = signal(0);
As you can see, our signal is in a separate file, so we can import it in any component that needs it.
Let's create two components that use the count
signal.
// app.component.ts
import { Component } from "@angular/core";
import { count } from "./main";
@Component({
selector: "app",
template: `
<div>
<p>Counter: {{ count() }}</p>
<button (click)="increment()">Increment from HundredIncrComponent</button>
</div>
`,
})
export class HundredIncrComponent {
count = count;
increment() {
this.count.update((value) => value + 100);
}
}
Here, we imported the count
signal and used it in this component, and we could do the same with any other component in the application.
In some simple scenarios, this might very well be all that you need.
However, I think that for most applications, this approach will not be sufficient.
This is a bit like using a global mutable variable in Javascript.
Anyone can mutate it, or in the case of signals, emit a new value by calling
set()
on it.
I think that in most cases, this is not a good idea, for the same reasons that a global mutable variable is usually not a good idea.
Instead of giving unrestricted access to anyone to this signal, we want to make sure that the access to this signal is somehow encapsulated and kept under control.
How to create signal-based reactive data services
The simplest pattern to share a writeable signal across multiple components is to wrap the signal in a data service, like so:
@Injectable({
providedIn: "root",
})
export class CounterService {
// this is the private writeable signal
private counterSignal = signal(0);
// this is the public read-only signal
readonly counter = this.counterSignal.asReadonly();
constructor() {
// inject any dependencies you need here
}
// anyone needing to modify the signal
// needs to do so in a controlled way
incrementCounter() {
this.counterSignal.update((val) => val + 1);
}
}
This pattern is very similar to the use of Observable Data Services with RxJs and a BehaviorSubject (see this guide on it), if you are familiar with that pattern.
The difference is that this service is much more straightforward to understand, and there are less advanced concepts at play here.
We can see that the writeable signal counterSignal is kept private from the service.
Anyone needing the value of the signal can get it via its public read-only counterpart, the counter member variable.
And anyone needing to modify the value of the counter can only do so in a controlled way, via the incrementCounter public method.
This way, any validations or error-handling business logic can be added to the method, and no one can bypass them.
Imagine that there is a rule that says that the counter cannot go above 100.
With this pattern, we can easily implement that on the incrementCounter method in one single place, rather than repeating that logic everywhere in the application.
We can also refactor and maintain the application better.
If we want to find out all parts of the application that are incrementing the counter, we just have to find the usage of the incrementCounter method using our IDE.
This type of code analysis would not be possible if we would just give direct access to the signal.
Also, if the signal needs access to any dependencies to work properly, those can be received in the constructor, like in any other service.
One of the principles at play here that makes this solution more maintainable is the principle of encapsulation.
We don't want just any part of the application to be able to emit freely new values for the signal, we want to allow that only in a controlled way.
So in general, if you have a signal shared across multiple components, for most cases you are probably better off using this pattern, rather than giving access to the writeable signal directly.
Signals and OnPush components
OnPush
components are components that are only updated when their input properties change, or when Observables subscribed with the async
pipe emit new values.
They are not updated when their input properties are mutated.
Now OnPush
components are also integrated with signals.
When signals are used on a component, Angular marks that component as a dependency of the signal. When the signal changes, the component is re-rendered.
In the case of OnPush
components, they will also be re-rendered when a signal attached to it is updated:
@Component({
selector: "counter",
template: `
<h1>Counter</h1>
<p>Count: {{ count() }}</p>
<button (click)="increment()">Increment</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CounterComponent {
count = signal(0);
increment() {
this.count.update((value) => value + 1);
}
}
In this example, if we click on the Increment button, the component will be re-rendered, meaning that Signals are integrated directly with OnPush
.
This means that we no longer need to inject ChangeDetectorRef
and invoke
markForCheck
, to update an OnPush
component in this scenario.
Consider the below example that does not use signals:
@Component({
selector: "app",
standalone: true,
template: ` Number: {{ num }} `,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class ExampleComponent {
num = 1;
private cdr = inject(ChangeDetectorRef);
ngOnInit() {
setInterval(() => {
this.num = this.num + 1;
this.cdr.markForCheck();
}, 1000);
}
}
As you can see, this is all a lot more complicated for the same equivalent functionality. The signal-based version is much simpler.
Can I create signals outside of components/stores/services?
Absolutely! You can create signals anywhere you want. No constraint says that a signal needs to be inside a component, store, or service.
We demonstrated that earlier. That is the beauty and power of Signals. In most cases though, you probably want to wrap the signal in a service, as we have seen.
How do Signals compare to RxJs?
Signals are not a direct replacement for RxJs, but they provide an easier-to-use alternative in certain situations where we would commonly need RxJs.
For example, signals are a great alternative to RxJS Behavior Subjects when it comes to transparently propagating data changes to multiple parts of the application.
I hope that you enjoyed this post, to get notified when new similar posts like this are out, I invite you to subscribe to our newsletter:
You will also get up-to-date news on the Angular ecosystem.
And if you want a deep dive into all the features of Angular Core like signals
and others, have a look at the Angular Core Deep Dive Course:
Summary
In this guide, we explored the Angular Signals API and the concept of a new reactive primitive: the signal.
We have learned that by using signals to track all our application states, we are enabling Angular to easily know what parts of the view need to be updated.
In summary, the main goals of Signals are:
- providing a new reactive primitive that is easier to understand, to enable us to build applications in a reactive style much more easily.
- avoiding unnecessary re-rendering of components that don't need to be re-rendered.
- avoiding unnecessary checks of components whose data didn't change.
The signals API is straightforward to use, but beware of some common pitfalls that you might run into:
- when defining effects or computed signals, be careful when reading the value of source signals inside conditional blocks
- when using array or object signals, avoid mutating the signal value directly
- don't overuse advanced features like providing your equality functions or doing manual effect cleanup unless it's necessary. The default behaviors should work just fine in the majority of cases.
I invite you to try out Signals in your application, and see the magic for yourself!