Angular Signals mark a significant shift for the framework, not an optional feature. Learn more so you can keep up!
As developers, we often get to choose which features we want to master. However, every once in a while, a feature comes along that fundamentally changes how a framework works, redefining the entire developer experience.
Let me explain what I mean. For example, if your project doesn’t use SSR, you can choose not to learn it. But Angular Signals is different; it’s not optional. It’s becoming the backbone of every modern Angular application. To stay productive and efficient, mastering it will be essential.
In this article, I will give you a quick introduction to the core concepts behind Angular Signals. You’ll learn about:
Let’s dive in. Angular Signals were introduced to simplify and modernize Angular’s reactivity model. They offer a fine-grained, predictable and framework-integrated approach to managing and reacting to state changes, eliminating the need for complex change detection mechanisms, NgZone or manual RxJS subscriptions.
With Signals, Angular enables local change detection, meaning only the components affected by a state change are updated. This is a significant shift from the traditional model, where Angular runs the change detector for the entire component tree on every update. As a result, the framework becomes significantly more efficient, reactive and developer-friendly.
If I had to summarize the paradigm shift introduced by Angular Signals, it would come down to how they affect the following key points:
There are three core concepts in Angular Signals:

Essentially, these are functions for working with signals. Let’s explore each one in detail.
A signal is a wrapper around any value that notifies interested consumers. You can wrap any value within a signal. Those values could be
Angular supports two types of signals:
You can create a writable signal using the signal() function and a read-only signal using the computed() function.
You can use the signal() function to create writable signals.
count : WritableSignal<number> = signal(0);
On the template, you can use the count signal as shown below:
<p>count = {{count()}}</p>
As you can see, we call the count signal as a function in the template because accessing a signal’s value requires invoking it like a function.
The writable signal has two methods:
set() – to directly set the signal to a new valueupdate() – to update the value of the signal based on its current valueBoth these methods notify interested consumers.
updateCount() {
this.count.update((current) => current + 1);
}
setCount() {
this.count.set(5);
}
Besides these two methods, writable signals also have one more function called asReadonly().
set() or update() function.asReadonly().When you try to update the value with the update() method, Angular gives you compilation error.

Now, let’s look at an example to understand how deep mutations of a signal’s value cannot be prevented even when using asReadonly().
Let’s assume you have a signal defined as shown below. When you attempt to update it as illustrated earlier, Angular will throw an error indicating that the operation isn’t allowed.
private userSignal: WritableSignal<any> = signal({
id: 1,
name: 'dj',
profile: {
email: 'dhananjay@nomadcoder.ai',
mode: ['light-mode', 'notifications']
}
});
updateUser(){
this.user = { ...this.user(), name: 'dhananjay' }; // TypeScript error
}
Angular should give you an error as shown below:

However, you should be able to perform the deep mutation of the signal value, as shown below:
updateUser(){
this.user().name = 'dhananjay';
}
So, in summary:
asReadonly() prevents calling .set() or .update() on the signal.asReadonly() prevents reassigning the signal reference.asReadonly() does not prevent mutating object properties.asReadonly() does not prevent modifying array contents.asReadonly() does not prevent deep or nested value changes.You can use the computed() function to create computed signals. These signals derive their values from existing signals.
Some key characteristics of computed signals include:
isEven: Signal<boolean> = computed(() => {
if (this.count() % 2 == 0) {
return true;
}
else {
return false;
}
})
On the template, you can use the isEvent computed signal as shown below:
<p>count = {{count()}} is {{isEven() ? 'even':'odd'}}</p>
One of the most important characteristics of computed signals is that they are not live consumers. This means Angular does not compute their value unless they are used within a reactive context, such as a template.
Consider the example below:
count = signal(10);
isEven: Signal<boolean> = computed(() => {
console.log('isEven computed called');
if (this.count() % 2 == 0) {
return true;
}
else {
return false;
}
})
Let’s assume the isEven computed signal is not used in the template. In this case, you’ll notice that Angular does not log any message from the computed function. This indicates that the computed signal code is not executed because it is not consumed anywhere in the reactive context.
Also, Angular does not allow writing to other signals within a computed signal. Let’s consider the example below:
count = signal(10);
a = signal(6);
isEven: Signal<boolean> = computed(() => {
console.log('isEven computed called');
this.a.set(99);
if (this.count() % 2 == 0) {
return true;
}
else {
return false;
}
})
So when writing to a signal inside a computed signal, Angular throws a run-time error as shown in the image below:

So, to summarize:
Based on our understanding of signals and computed signals, let’s consider a practical example—implementing a Country–State dropdown scenario. While implementing, we need to:
In the final requirement, we need to set the state value manually. If it is bound to a computed signal, this is not possible because computed signals are read-only and cannot be directly set or updated.
To solve the above problem, Angular provides a new type of signal called LinkedSignal.
Linked signals are writable computed signals, meaning they derive their values from other signals but can still be updated directly.
Let’s say you have a signal based on the book array as shown below.
bookData: IBook[] = [
{
Id: 1,
Title: 'Angular Essentials',
},
{
Id: 2,
Title: 'React Masterclass',
},
];
books: WritableSignal<IBook[]> = signal<IBook[]>(this.bookData);
You need to manage the user’s selected book, and for that you can create a selectedBook linked signal that:
books signalbooks signal changesselectedBook = linkedSignal({
source: this.books,
computation: (a) => {
console.log('hello');
return a[0];
},
});
You create a linked signal using the linkedSignal function.
linkedSignal factory function.source signal and the computation function.selectedBook = linkedSignal(() => this.books()[0]);
Like writable signals, linked signals also provide set and update methods. You can manually set the value of the selectedBook linked signal using the set method, as shown in the following code example.
updateSelectedBook() {
this.selectedBook.set({
Id: 1,
Title: 'You Do not know JavaScript',
});
}
Also, whenever the value of the books signal is updated, the value of the **selectedBook** linked signal is recomputed and set to the new value. As shown in the following code listing, the books signal is set to the new books array, and the value of selectedBook linkedSignal will be automatically recomputed to the new book .NET.
changeBook() {
this.bookData = [
{
Id: 1,
Title: '.NET',
},
{
Id: 2,
Title: 'Java',
},
{
Id: 3,
Title: 'Angular Essentials',
},
];
this.books.set(this.bookData);
}
You can use the linkedSignal in the template, precisely like a signal or computed signal
<ul>
@for (book of bookData; track book.Title) {
<li>{{book.Title}}</li>
}
</ul>
<h3>Selected Books : {{seletedBook().Title}}</h3>
<button (click)="changeBook()">Change Books</button>
<button (click)="updateSelectedBook()">update selected book</button>
Signals are useful because they notify any interested consumers when their values change. An effect is an operation that automatically runs whenever one or more of those signal values update. You can create an effect using the effect factory method.
An effect can be created using the effect factory method.
CountSignal = signal<number>(0);
constructor() {
effect(() => {
console.log('effect called for CountSignal ', this.CountSignal());
})
}
Angular reruns an effect whenever any of the signals it depends on change. An effect is a side-effect–producing function that reads one or more signals and is automatically scheduled to run again when those signals update. Unlike a computed signal, an effect is not executed immediately; it is queued to run later.
Every effect is guaranteed to run at least once.
Angular defines two types of effects, distinguished by the way each one is scheduled for execution.
Root effects are top-level effects defined within a service. They are:
ApplicationRef.tickRoot effects are best for handling work that isn’t tied to any specific component. They help you update other signals, sync data with a backend or local storage, run global rendering tasks, and do things like logging or debugging.
View effects are created within a component and scheduled to execute during the next change detection cycle.
Angular only allows effects to be created inside an injection context, since the effect() function requires one. This means you can create an effect in a component’s constructor, as a component property or by manually providing an injector.
In the constructor, an effect can be created as below:
CountSignal = signal<number>(0);
constructor() {
effect(() => {
console.log('effect called for CountSignal ', this.CountSignal());
)
}
An effect can be created as a property, as shown below:
CountSignal = signal<number>(0);
CounterEffect = effect(()=>{
console.log('effect called for CountSignal ', this.CountSignal());
})
You can also create an effect inside a custom function by explicitly passing the injector, as shown in the example below.
private injector = inject(Injector);
CountSignal = signal<number>(0);
log() {
effect(() => {
console.log('effect called for CountSignal ', this.CountSignal());
}, { injector: this.injector });
}
Effects created in components, directives or services are normally destroyed with them because their life span follows Angular’s DestroyRef, but you can override this by enabling manualCleanup.
countEffectRef: EffectRef;
CountSignal = signal<number>(0);
constructor() {
this.countEffectRef = effect(() => {
console.log('effect called for CountSignal ', this.CountSignal());
})
}
ngOnDestroy() {
if (this.countEffectRef) {
this.countEffectRef.destroy();
}
}
Effects are not needed very often, but they can be helpful in some instances, such as:
localStorage<canvas>To propagate data changes, use computed signals rather than effects.
All Angular signal factory methods—such as signal(), computed() and linkedSignal() accept an optional equality function. This function allows you to define how Angular should determine whether a signal’s new value is the same as its previous value.
To understand the purpose of the equality function, let’s assume you have a signal defined as shown in the next code listing.
product : IPrduct = {
id: 1,
name : 'Laptop',
price : 50000,
description: 'A high performance laptop'
}
ProductSignal = signal<IPrduct>(this.product);
And IProduct is defined as the next code listing:
export interface IPrduct{
id:number;
name : string;
price : number;
description?: string;
}
And then if you create an effect to read the value change in the Product Signal:
productEffect = effect(() => {
console.log('effect called');
console.log(this.ProductSignal());
});
And update the signal with the same value of the Product:
setProduct() {
let p: IPrduct =
{
id: 1,
name: 'Laptop',
price: 50000,
description: 'A high performance laptop'
};
this.ProductSignal.set(p);
}
You will notice that even when you assign the same value as the signal’s previous value, Angular still treats it as a new value and propagates the update to the effect and the entire reactive context.
To control how Angular determines whether the previous and current values are considered the same, you can define your own equality function.
function ProductSignalEqualityFunction(previous: IPrduct, current: IPrduct) {
console.log("previous value",previous);
console.log("current value",current);
if (JSON.stringify(previous) === JSON.stringify(current)) {
return true;
}
return false;
}
The signal equality function is a simple function that takes two parameters—the previous and current signal values. You can implement your own logic inside it to decide whether the two values should be treated as the same or not.
You can use the equality function by passing it as the second parameter to the signal, computed and linkedSignal functions. So, we can pass it to the ProductSignal as shown below,
ProductSignal = signal<IPrduct>(this.product,{equal: ProductSignalEqualityFunction});
Now, when you update the signal with the same product object value, the effect will not run and the changes will not propagate to the relevant reactive context.
In this article, you learned everything needed to write a modern Angular app using signals. We covered signals, computed signals, linked signals and effects. I hope you find this helpful. Thanks for reading.
Dhananjay Kumar is a well-known trainer and developer evangelist. He is the founder of NomadCoder, a company that focuses on creating job-ready developers through training in technologies such as Angular, Node.js, Python, .NET, Azure, GenAI and more. He is also the founder of ng-India, one of the world’s largest Angular communities. He lives in Gurgaon, India, and is currently writing his second book on Angular. You can reach out to him for training, evangelism and consulting opportunities.