Angular Signals – signal, computed and effect
🎧 Focus sounds
Soft beat sounds that many learners like while reading. Pick one, press Play, and keep studying.
Keep the volume low. These sounds are for comfort while studying, not a medical treatment. If you have epilepsy or are sensitive to pulsing sounds, use the background noise only.
In this lesson
Signals are Angular’s modern way to hold state. A signal is a value that notifies anything reading it when it changes, so Angular knows exactly which parts of the page to update.
Creating and reading signals#
import { signal } from '@angular/core';
const count = signal(0); // create with an initial value
console.log(count()); // read by CALLING it: 0
count.set(5); // replace the value
count.update(n => n + 1); // compute from the current value: 6
In a template, call it too: {{ count() }}. When the signal changes, that spot in the template updates.
computed: derived values#
import { computed, signal } from '@angular/core';
const price = signal(500);
const quantity = signal(2);
const total = computed(() => price() * quantity()); // 1000
quantity.set(3);
console.log(total()); // 1500: recalculated automatically
- A computed signal tracks the signals it reads and recalculates only when one of them changes.
- It’s lazy and cached: reading
total()ten times computes once. - It’s read-only: you can’t
seta computed.
effect: side effects#
An effect runs when signals it reads change. Use it to sync with things outside Angular (localStorage, logging, a chart library):
import { effect } from '@angular/core';
effect(() => {
localStorage.setItem('cart', JSON.stringify(items())); // re-runs whenever items changes
});
Effects must be created in an injection context (a constructor or field initialiser of a component/service). Don’t use effects to set other signals: that’s what computed is for.
A signal-based cart#
// src/app/cart.store.ts
import { Injectable, computed, signal } from '@angular/core';
export interface CartItem { id: number; title: string; price: number; qty: number; }
@Injectable({ providedIn: 'root' })
export class CartStore {
private readonly _items = signal<CartItem[]>([]);
readonly items = this._items.asReadonly(); // components can read, not write
readonly count = computed(() => this._items().reduce((n, i) => n + i.qty, 0));
readonly total = computed(() => this._items().reduce((s, i) => s + i.price * i.qty, 0));
readonly coupon = signal<string | null>(null);
readonly payable = computed(() => (this.coupon() === 'LEARN20' ? Math.round(this.total() * 0.8) : this.total()));
add(course: { id: number; title: string; price: number }) {
this._items.update(items => {
const found = items.find(i => i.id === course.id);
return found
? items.map(i => (i.id === course.id ? { ...i, qty: i.qty + 1 } : i)) // new array, new object
: [...items, { ...course, qty: 1 }];
});
}
remove(id: number) {
this._items.update(items => items.filter(i => i.id !== id));
}
clear() {
this._items.set([]);
this.coupon.set(null);
}
}
// src/app/cart-view/cart-view.component.ts
import { Component, inject } from '@angular/core';
import { CartStore } from '../cart.store';
@Component({
selector: 'app-cart-view',
standalone: true,
template: `
<button (click)="cart.add({ id: 1, title: 'Java', price: 500 })">Add Java</button>
<button (click)="cart.add({ id: 2, title: 'Angular', price: 600 })">Add Angular</button>
<button (click)="cart.coupon.set('LEARN20')">Apply LEARN20</button>
<p class="summary">{{ cart.count() }} items · ₹{{ cart.payable() }}</p>
<ul>
@for (item of cart.items(); track item.id) {
<li>{{ item.title }} × {{ item.qty }} <button (click)="cart.remove(item.id)">Remove</button></li>
}
</ul>
`,
})
export class CartViewComponent {
cart = inject(CartStore);
}
Add Java twice and Angular once: 3 items · ₹1600. Apply LEARN20: 3 items · ₹1280. The service is the single source of truth; any component that injects CartStore sees the same cart.
Immutability still matters#
Signals compare values with Object.is by default. If you mutate an array in place (items().push(x)) and set the same array, readers aren’t notified. Always produce new arrays/objects in update, just like React state.
Signals and RxJS#
Angular apps also use RxJS Observables (for HTTP, events over time). Convert between them with @angular/core/rxjs-interop:
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
courses = toSignal(this.http.get<Course[]>('/api/courses'), { initialValue: [] });
You’ll use this in lesson 10.
Rule of thumb in modern Angular: signals for state (what the UI shows), RxJS for events and async streams (HTTP, debounced search, websockets), and toSignal to bring stream results into templates.
Exercise#
- Create a counter component with
count,double = computed(...)and +1 / reset buttons. - Add a
remainingcomputed signal to a to-do list that counts unfinished tasks. - Persist the cart to localStorage with an
effect, and load it in the signal’s initial value. - Add a
discountPercentsignal and makepayableuse it.
Hint for 3
private readonly _items = signal<CartItem[]>(JSON.parse(localStorage.getItem('cart') ?? '[]')); and in the constructor effect(() => localStorage.setItem('cart', JSON.stringify(this._items())));
Interview corner#
- What is a signal? A reactive value container; reading it registers a dependency, and changes notify dependents.
- signal vs computed vs effect? Writable state; derived read-only state; side effects reacting to signal changes.
- Why are signals good for performance? Angular knows exactly which views depend on which signals, enabling fine-grained updates (and zoneless change detection).
- set vs update?
setreplaces the value;updatecomputes the new value from the current one.
Summary#
signal(initial), read withx(), change withset/update.computedderives cached values;effectsyncs with the outside world.- Keep updates immutable; put shared signal state in services.
Next: services and dependency injection.
Quick quiz
3 questions. Check what stuck and earn 2 XP for each right answer.
Progress is saved in this browser. Create a free account to keep it on every device.
Get new lessons in your inbox
One email when we publish a new tutorial or guide. No spam, unsubscribe in one click.