Angular Signals
By Syncfusion®, Inc. · Last updated
Storing and updating values with signals: writable signals, reading and updating values, computed signals, effects, signals compared with RxJS, and best practices.
What are Signals?
A signal is a box that stores one value. When the value in the box changes, Angular updates every part of the screen that reads it.
You create a signal with the signal() function, and you read its value by calling it like a function: count().
Real-World Example
On the cart page of an online store, the quantity of each item is stored in a signal. The subtotal, tax and total are calculated from the quantities.
When the user changes a quantity, Angular updates only the numbers that depend on it. The rest of the page stays as it is.
price = 50writable signalqty = 3writable signaltotal = 150computedTemplateshows Total: 150effect()writes a log lineChange qty and Angular recalculates total, then updates the template and runs the effect.
Signals pass a change to everything that reads them.
Why Signals?
In earlier lessons, normal fields updated the screen after a click. But a normal field does not update the screen when its value changes later, for example in a timer or after a server answer. A signal always updates the screen, whenever its value changes.
Benefits of signals:
- The screen always shows the current value, after a click, a timer or a server answer.
- Angular knows exactly what to update. It updates only the parts of the screen that read the changed signal.
- Values can depend on other values. A computed signal calculates its value from other signals automatically.
- Simple code. You read with
count()and change withset()orupdate(). You do not needsubscribe().
| Point | Normal field | Signal |
|---|---|---|
| Create | count = 0 | count = signal(0) |
| Read | count | count() |
| Change | count = 5 | count.set(5) |
| Screen updates after a timer or server answer | No | Yes |
Writable Signals
A writable signal is a signal whose value you can change. You create it with signal() and give it a starting value:
import { signal } from '@angular/core';
quantity = signal(1); // a number
userName = signal('Guest'); // a string
items = signal<string[]>([]); // an empty list of strings
For an empty list, write the type in < >, for example signal<string[]>([]). Otherwise TypeScript does not know what the list will contain.
Reading Signal Values
To read the value, call the signal like a function, with ():
- In the template:
{{ quantity() }} - In the class:
this.quantity()
Creating and reading signals
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<p>Product: {{ product() }}</p>
<p>Price: {{ price() }}</p>
<p>{{ stockMessage() }}</p>
`,
})
export class App {
product = signal('Wireless Mouse');
price = signal(15);
stock = signal(0);
stockMessage() {
return this.stock() > 0 ? 'In stock' : 'Out of stock';
}
}
The method stockMessage() reads the stock signal with this.stock().
If you forget the (), you get the signal box, not its value. {{ price }} shows [Signal: 15] on the page, not 15.
Updating Signal Values
A writable signal has two methods to change its value:
set(value)update(fn)quantity = 3quantity.set(1)the old value is ignoredquantity = 1quantity = 3quantity.update(q => q + 1)uses the old valuequantity = 4
set() replaces the value. update() calculates it from the current value.
set(value)replaces the value with a new value. Example:quantity.set(1).update(fn)calculates the new value from the current value. The function gets the current value and returns the new one. Example:quantity.update(q => q + 1).
Use set() when the new value does not depend on the old one. Use update() when it does.
You cannot change a signal with =. this.quantity = 5 is an error, because quantity is a signal, not a number. Always use set() or update().
set() and update()
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<p>Quantity: {{ quantity() }}</p>
<button (click)="decrease()">-</button>
<button (click)="increase()">+</button>
<button (click)="reset()">Reset</button>
`,
})
export class App {
quantity = signal(1);
increase() {
this.quantity.update(q => q + 1);
}
decrease() {
this.quantity.update(q => (q > 1 ? q - 1 : 1));
}
reset() {
this.quantity.set(1);
}
}
Updating Lists and Objects
A signal reports a change only when it gets a new value. If you change the old list with push(), the signal still holds the same list, so it reports no change. Then a computed signal that reads the list, such as a count, keeps its old value.
Create a new list instead. [...list, item] copies the old items into a new list and adds the new item at the end.
Updating a list in a signal
import { Component, computed, signal } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<button (click)="add('Notebook')">Add notebook</button>
<button (click)="add('Pen')">Add pen</button>
<button (click)="clear()">Clear</button>
<p>Items ({{ count() }}): {{ items().join(', ') }}</p>
`,
})
export class App {
items = signal<string[]>([]);
count = computed(() => this.items().length);
add(name: string) {
this.items.update(list => [...list, name]);
}
clear() {
this.items.set([]);
}
}
count is a computed signal. You learn computed signals in the next section.
For an object, copy it in the same way: user.update(u => ({ ...u, name: 'Asha' })) creates a new object with a new name.
Computed Signals
A computed signal calculates its value from other signals. You create it with computed() and a function:
total = computed(() => this.price() * this.quantity());
- Angular runs the function again only when
priceorquantitychanges. - If they do not change, Angular reuses the last result. So reading
total()many times is fast. - A computed signal is read-only. It has no
set()orupdate().
A cart total with computed()
import { Component, computed, signal } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<p>Keyboard: $40 × {{ quantity() }}
<button (click)="quantity.update(q => q + 1)">+1</button></p>
<p>Subtotal: {{ subtotal() }}</p>
<p>Tax (10%): {{ tax() }}</p>
<p><b>Total: {{ total() }}</b></p>
`,
})
export class App {
price = signal(40);
quantity = signal(1);
subtotal = computed(() => this.price() * this.quantity());
tax = computed(() => this.subtotal() * 0.1);
total = computed(() => this.subtotal() + this.tax());
}
Click +1. subtotal changes because quantity changed. tax and total change because they read subtotal.
Effects
An effect is a function that Angular runs again each time a signal that it reads changes. You create it with effect().
theme.set("dark")a signal changesAngularfinds effects that read iteffect(() => ...)runs againSide effectlog, save, titleEffects are for work outside the screen. Values for the screen come from computed().
An effect runs again each time a signal it reads changes.
Use an effect for work outside the template, for example:
- Writing a log message.
- Saving a value in the browser's storage.
- Changing the browser tab title.
Create the effect in the constructor. Like inject(), effect() works only while Angular creates the class.
An effect writes a log line
import { Component, effect, signal } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<button (click)="theme.set('light')">Light</button>
<button (click)="theme.set('dark')">Dark</button>
<p>Current theme: {{ theme() }}</p>
@for (line of log(); track $index) {
<p>{{ line }}</p>
}
`,
})
export class App {
theme = signal('light');
log = signal<string[]>([]);
constructor() {
effect(() => {
const value = this.theme();
this.log.update(list => [...list, `Theme saved: ${value}`]);
});
}
}
The effect runs once at the start, and again each time you click a different theme.
Do not use an effect to calculate a value from other signals. Use computed() for that. An effect is only for work outside the screen, such as logging or saving.
Signals vs RxJS
You learned RxJS Observables in the previous chapter. Signals and Observables solve different problems, and Angular apps use both.
Signal: one current valueObservable: values over timecount = 3read any time: count()123subscribe() receives each value when it happens
Signals hold state. Observables deliver events over time.
| Point | Signal | Observable (RxJS) |
|---|---|---|
| Holds | One current value, always | A series of values over time |
| Read the value | count(), at any time | subscribe() or the async pipe |
| Has a value at the start | Yes | Not always |
| Clean up | Not needed | Unsubscribe from Observables that never complete |
| Best for | Data shown on the screen | Events and server requests, with operators such as switchMap() |
A simple rule: store data in signals, and use RxJS for events over time, such as a search box that sends requests.
Signals Best Practices
- Call the signal with
()to read its value, in both the template and the class. - Use
computed()for values that come from other signals, for example a total from a price and a quantity. Do not copy the result into another writable signal. - Create new lists and objects in
update(). Do not change them withpush()or by setting a property. - Use effects only for work outside the screen, such as logging and saving.
- Keep writable signals private in services. Give other classes a read-only version with
asReadonly(), and change the value only with the service's methods:
export class CartService {
private readonly _count = signal(0);
readonly count = this._count.asReadonly(); // other classes can read, not set
add() {
this._count.update(n => n + 1);
}
}
Common Mistakes
| Mistake | What you see | Fix |
|---|---|---|
{{ price }} without () | The page shows [Signal: 15], not 15 | Write {{ price() }} |
items().push('Pen') | Computed signals that read the list keep their old value | Use items.update(list => [...list, 'Pen']) |
total.set(10) on a computed signal | Error: set is not a function | Change the signals that total reads |
| Calculating a value inside an effect | Extra code, and values can be one step behind | Use computed() |
Calling effect() in a click method | Error: effect() must be called from an injection context | Create effects in the constructor |
Check Yourself
Exercises
What to Remember
- A signal stores one value. Create it with
signal(), read it with(), and change it withset()orupdate(). For lists and objects, always create a new one. computed()calculates a read-only value from other signals and recalculates only when they change.effect()runs code outside the screen when signals change. Use signals for data on the screen, and RxJS for events over time.
Next: State Management, how to store data that many components share.