Signals
Signals are Angular’s modern way to hold reactive state. A signal is a value that tells Angular when it changes, so exactly the right parts of the screen update — simply and efficiently.
On this page · 6 min · Intermediate
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Create and update signals
- Derive values with computed
- Run side effects with effect
- Use linkedSignal and convert to and from Observables
Explanation#
signal(initial) creates writable state. Read it by calling it: count(). Change it with set(value) or update(fn).
computed(() => ...) derives a read-only value from other signals. It is lazy and cached: it recalculates only when a signal it read changes.
effect(() => ...) runs a side effect whenever the signals it reads change — saving to localStorage, logging, syncing a chart. Do not use effects to set other state when computed would do.
linkedSignal(() => source()) is writable state that resets when its source changes — e.g. a selected item that resets when the list changes.
Update objects and arrays immutably: items.update(list => [...list, item]). Mutating in place does not notify Angular.
Inputs (input()), models (model()) and queries (viewChild()) are signals too. toSignal and toObservable bridge to RxJS.
Syntax#
count = signal(0);
double = computed(() => this.count() * 2);
constructor() { effect(() => localStorage.setItem('count', String(this.count()))); }
increment() { this.count.update(c => c + 1); }
Code example: A cart built entirely on signals#
import { Component, computed, effect, linkedSignal, signal } from '@angular/core';
import { CurrencyPipe } from '@angular/common';
type Item = { id: number; name: string; price: number; qty: number };
@Component({
selector: 'app-signal-cart',
imports: [CurrencyPipe],
template: `
@for (item of items(); track item.id) {
<p>
<label><input type="radio" name="sel" [checked]="selectedId() === item.id" (change)="selectedId.set(item.id)" /></label>
{{ item.name }} × {{ item.qty }}
<button (click)="increase(item.id)">+</button>
</p>
}
<p>Selected: {{ selected()?.name ?? 'none' }}</p>
<p>Items: {{ count() }} · Total: {{ total() | currency: 'INR' }} @if (freeDelivery()) { · 🚚 free delivery }</p>
<button (click)="loadOtherCart()">Load another cart</button>
`,
})
export class SignalCart {
items = signal<Item[]>([
{ id: 1, name: 'Pen', price: 20, qty: 2 },
{ id: 2, name: 'Notebook', price: 60, qty: 1 },
]);
count = computed(() => this.items().reduce((n, i) => n + i.qty, 0));
total = computed(() => this.items().reduce((s, i) => s + i.qty * i.price, 0));
freeDelivery = computed(() => this.total() >= 500);
// Resets to the first item whenever the items list is replaced
selectedId = linkedSignal(() => this.items()[0]?.id ?? null);
selected = computed(() => this.items().find(i => i.id === this.selectedId()));
constructor() {
effect(() => console.log(`Cart now has ${this.count()} items worth ₹${this.total()}`));
}
increase(id: number) {
this.items.update(list => list.map(i => (i.id === id ? { ...i, qty: i.qty + 1 } : i)));
}
loadOtherCart() {
this.items.set([{ id: 3, name: 'Backpack', price: 1200, qty: 1 }]);
}
}
The cart shows Pen × 2 and Notebook × 1, “Selected: Pen” and “Items: 3 · Total: ₹100.00”. Clicking + updates the line, count and total together and logs “Cart now has 4 items worth ₹120”. “Load another cart” replaces the items; the selection automatically resets to Backpack and “🚚 free delivery” appears because the total passes ₹500.
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- items is the only writable state; count, total and freeDelivery are computed from it.
- increase() uses update with map and spread — a new array, so Angular notices.
- selectedId is a linkedSignal: users can change it, but it resets when items is replaced.
- The effect logs whenever count or total change.
- The template reads signals by calling them, and Angular updates only what depends on them.
Real-world use case#
A product configurator keeps size, colour and quantity in signals; price, stock warnings and the preview image are computed — no manual subscriptions or change-detection tricks.
Common mistakes#
items().push(x) changes the array without notifying Angular. Use update(list => [...list, x]).
effect(() => this.total.set(...)) creates loops and extra renders. Use computed.
{{ count }} shows the function, not the value. Write {{ count() }}.
Best practices#
- Minimal writable signals, everything else computed
- Immutable updates
- Effects only for side effects
- Expose readonly signals from services
Remember this#
signal = state. computed = derived. effect = side effect.
Read with x(); write with set/update.
linkedSignal = writable value that resets when its source changes.
Practice exercise#
Your task: Create a temperature converter with a celsius signal and a computed fahrenheit, plus an effect that logs both whenever celsius changes.
Need a hint?
Show solution
import { Component, computed, effect, signal } from '@angular/core';
@Component({
selector: 'app-converter',
template: `
<input type="number" [value]="celsius()" (input)="setCelsius($event)" />
<p>{{ celsius() }}°C = {{ fahrenheit() }}°F</p>
`,
})
export class Converter {
celsius = signal(25);
fahrenheit = computed(() => this.celsius() * 9 / 5 + 32); // 77
constructor() {
effect(() => console.log(`${this.celsius()}°C = ${this.fahrenheit()}°F`));
}
setCelsius(event: Event) {
this.celsius.set(Number((event.target as HTMLInputElement).value));
}
}
Mini quiz#
Which creates a read-only value derived from other signals?
Want more? Practise all Angular MCQs · Angular interview questions
My notes#
Saved in this browser.