Signals & computed
A signal is a box that holds a value and tells Angular whenever that value changes. Because Angular knows exactly which parts of the page read which signals, it can update only those parts. Signals are the foundation of modern Angular: component state, inputs, forms and data loading are all built on them.
In this lesson you will create signals, change them correctly, and build values that are calculated from other signals with computed().
Creating and reading a signal
Import signal from @angular/core and give it a starting value. To read the value, call the signal like a function:
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-attendance',
template: `
<p>Students present: {{ present() }}</p>
<button (click)="markPresent()">Mark one present</button>
<button (click)="reset()">Reset</button>
`,
})
export class Attendance {
protected present = signal(0);
protected markPresent() {
this.present.update((n) => n + 1);
}
protected reset() {
this.present.set(0);
}
}A signal created with signal() is a writable signal. It has two methods for changing its value:
set(newValue)replaces the value:this.present.set(0).update(fn)calculates the new value from the old one:this.present.update((n) => n + 1).
When the value changes, every template and every computed that read this signal is notified. TypeScript infers the type from the starting value, so present is a WritableSignal<number>. When the starting value does not show the full type, give it in angle brackets: signal<string | null>(null) or signal<Order[]>([]).
Derived values with computed()
Many values depend on other values: the total of a bill, the number of unread messages, whether a form can be submitted. Do not store these in separate signals and try to keep them in sync. Use computed():
import { computed, signal } from '@angular/core';
const items = signal([
{ name: 'Samosa', price: 20, qty: 2 },
{ name: 'Masala Chai', price: 15, qty: 1 },
]);
const subtotal = computed(() =>
items().reduce((sum, item) => sum + item.price * item.qty, 0),
);
const delivery = computed(() => (subtotal() >= 200 ? 0 : 30));
const total = computed(() => subtotal() + delivery());
console.log(subtotal(), delivery(), total());
items.update((list) => [...list, { name: 'Veg Thali', price: 120, qty: 1 }]);
console.log(subtotal(), delivery(), total());55 30 85 175 30 205
A computed signal is read-only: it has no set() or update(). It reads other signals inside its function, and Angular records those reads as dependencies. When a dependency changes, the computed value is marked as stale.
Computed signals are also lazy and memoised (cached):
- Lazy: the function does not run until someone reads the computed value.
- Memoised: after it runs, the result is kept. Reading it again returns the cached value without running the function, until a dependency changes.
So it is cheap to read total() many times in a template. This is why the Templates lesson recommended computed signals over methods for derived values.
Do not mutate, replace
Signals decide whether something changed by comparing the old and new values with Object.is. For arrays and objects, that compares references (which object it is), not the contents. If you change an array in place, the reference stays the same, and nothing is notified:
const names = signal(['Aarav']);
const count = computed(() => names().length);
console.log(count());
names().push('Priya'); // wrong: mutates the array in place
console.log(count(), names().length);
names.update((list) => [...list, 'Kabir']); // right: a new array
console.log(count());1 1 2 3
After the push, the array really has two names, but count still says 1, and the page would still show the old list. Always create a new array or object: use the spread syntax [...list, item], filter(), map(), or { ...student, marks: 90 }.
The same comparison also saves work: setting a signal to a value equal to its current value (such as setting 'Pune' when it is already 'Pune') does not notify anyone.
Exposing signals safely
In a service or component, you often want other code to read a signal but not change it. Keep the writable signal private and expose a read-only view with asReadonly():
import { computed, signal } from '@angular/core';
export class Scoreboard {
private readonly runs = signal(0);
readonly score = this.runs.asReadonly();
readonly isFifty = computed(() => this.runs() >= 50);
hit(value: 1 | 2 | 4 | 6) {
this.runs.update((r) => r + value);
}
}Outside code can call score() and isFifty(), but the only way to change the runs is the hit() method, which checks that the value is a valid shot.
Advanced: custom equality and untracked reads
You can pass an equal function to decide when two values count as "the same". For example, treat two locations as equal if their pin codes match:
const location = signal(
{ city: 'Jaipur', pin: '302001' },
{ equal: (a, b) => a.pin === b.pin },
);Sometimes a computed signal needs to read a signal without depending on it. Wrap that read in untracked(): computed(() => name() + ' / ' + untracked(theme)) re-runs when name changes, but not when theme changes. Use this rarely; most of the time you want normal tracking.
Common mistakes
- Reading a signal without calling it.
if (this.present > 0)compares a function, not a number. Writethis.present(). - Mutating arrays or objects inside a signal.
list().push(x)orstudent().marks = 90changes data without notifying anyone. Create a new value withupdate(). - Storing derived values in writable signals. If
totalcan be calculated fromitems, make itcomputed, not a second signal that you must remember to update. - Trying to
set()a computed signal. Computed signals are read-only; change the signals they depend on instead.
Key points
signal(value)creates a writable signal; read it by calling it, change it withset()orupdate().computed(() => ...)creates a read-only value derived from other signals; it is lazy and cached.- Signals compare values with
Object.is, so always replace arrays and objects instead of mutating them. - Use
asReadonly()to share a signal without allowing outside changes. - Signals let Angular update only the parts of the page that depend on changed values.
Exercise
Write a FeeCalculator component for a coaching centre. Keep a signal subjects holding a list of { name, fee } objects (Maths ₹4,000, Physics ₹3,500). Add computed signals for the subject count, the total fee, and a 10% discount that applies only when there are 3 or more subjects. Add a button that adds Chemistry (₹3,500) and a button that removes the last subject. Show all values in the template.
Show solution
import { Component, computed, signal } from '@angular/core';
@Component({
selector: 'app-fee-calculator',
template: `
<p>Subjects: {{ count() }}</p>
<p>Fee: ₹{{ fee() }}</p>
<p>Discount: ₹{{ discount() }}</p>
<p>Pay: ₹{{ fee() - discount() }}</p>
<button (click)="addChemistry()">Add Chemistry</button>
<button (click)="removeLast()" [disabled]="count() === 0">Remove last</button>
`,
})
export class FeeCalculator {
protected subjects = signal([
{ name: 'Maths', fee: 4000 },
{ name: 'Physics', fee: 3500 },
]);
protected count = computed(() => this.subjects().length);
protected fee = computed(() => this.subjects().reduce((sum, s) => sum + s.fee, 0));
protected discount = computed(() => (this.count() >= 3 ? this.fee() * 0.1 : 0));
protected addChemistry() {
this.subjects.update((list) => [...list, { name: 'Chemistry', fee: 3500 }]);
}
protected removeLast() {
this.subjects.update((list) => list.slice(0, -1));
}
}At first the page shows a fee of ₹7500 and no discount. After adding Chemistry, the fee is ₹11000, the discount ₹1100, and the amount to pay ₹9900. Both buttons create new arrays ([...list, x] and slice), so every computed value updates.
Quick Quiz
1. How do you increase a writable signal score by 4 based on its current value?
update() receives the current value and returns the new one.
2. What is true about a computed signal?
Computed signals are read-only, lazy and cached until a dependency changes.
3. Why does list().push(item) not update the page?
Signals compare with Object.is; mutating in place keeps the same reference.
4. Which method gives other code a version of a signal that it can read but not change?
asReadonly() returns a read-only Signal view of a writable signal.
Learn Angular with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
