The one-sentence mental model
A signal holds a value and can notify reactive consumers when that value changes. Read it by calling its getter function; update a writable signal through its write methods.
A reactive consumer can be a template, a computed(), or an effect(). Angular tracks signal reads while that consumer executes. An ordinary call such as console.log(count()) reads the value without creating a subscription. Signals help Angular target view checking and work in both zone-based and zoneless apps.
Creating and reading a signal
You create one with the signal() function, giving it an initial value. You read it by calling it like a function.
import { signal } from '@angular/core';
// Create a writable signal with an initial value.
const count = signal(0);
// Read it by CALLING it — note the parentheses.
console.log(count()); // 0
// It's just a value container that tracks its readers.
const name = signal('Ada');
console.log(name()); // 'Ada'A writable counter
Every button calls set() or update(). Watch the read-out track the value instantly.
Writing: set() vs update()
There are exactly two ways to change a writable signal:
- •
set(value)— replace the value outright. Use it when the new value doesn't depend on the old one. - •
update(fn)— compute the next value from the current one. Use it for increments, toggles, and appends.
const count = signal(0);
// set(): replace the value outright.
count.set(10); // count() === 10
// update(): derive the next value from the current one.
count.update(n => n + 1); // 11
count.update(n => n * 2); // 22
// Toggling a boolean is the classic update() case.
const open = signal(false);
open.update(v => !v); // trueReading in a template
In a template you call the signal. Angular tracks the consuming view and can schedule it for checking when the signal changes. Other bindings in that view can also be evaluated; this is not an expression-by-expression rendering guarantee.
@Component({
template: `
<h1>Hello, {{ name() }}</h1>
<input [value]="name()" (input)="name.set($any($event.target).value)" />
`,
})
export class Greeting {
name = signal('Ada');
}
// Changing name marks its consuming view for checking, including other bindings.Live template read
Type below. The heading re-renders on every keystroke because it reads name().
Hello, Ada Lovelace 👋
Signals of objects & arrays
A signal can hold objects and arrays. Prefer immutable updates: create a new value rather than modifying the existing one. By default, writes compare values with Object.is. Mutating a nested property bypasses the signal write API and does not notify consumers. An unrelated view check may reveal the mutation while a cached computed remains stale.
const profile = signal({ first: 'Grace', last: 'Hopper' });
// ✅ Replace with a new object (spread the old, override a field).
profile.update(p => ({ ...p, first: 'Ada' }));
// ❌ Mutating in place does not notify consumers; cached derivations can go stale.
profile().first = 'Ada'; // don't do this
// Arrays follow the same rule:
const todos = signal<string[]>([]);
todos.update(list => [...list, 'Learn signals']); // ✅ new arrayImmutable object updates
Each edit spreads the old object into a brand-new one. The original is never touched.
Protecting state with read-only signals
A WritableSignal exposes set() and update(). Keep it private and expose .asReadonly() to remove those methods from the public API. This does not freeze contained objects: use readonly types and immutable updates to protect their shape too.
class ScoreService {
// Writable, but kept private to this class.
private _score = signal(0);
// Public, read-only view. Callers can read() but not set()/update().
readonly score = this._score.asReadonly();
award(points: number) {
this._score.update(s => s + points);
}
}
// service.score() ✅ read
// service.score.set(9) ❌ compile error — no set() on a read-only signalRead-only score
The template can only read score(). Points can only be awarded through the component's own methods.
🎯 Your turn: hit exactly 10
Here's a signal starting at 3. Using only the buttons, land it on exactly 10. The badge below is driven by a computed() (a preview of Module 2) that watches the value — no manual checking. Think about which button uses update() and how the derived state reacts.
Reach 10
Goal: target() === 10. Combine the steps.
Key takeaways
- ✓A signal notifies consumers that tracked its value in a reactive context.
- ✓Create with
signal(initial); read by calling it:count(). - ✓
set()replaces;update()derives from the current value. - ✓For objects/arrays, replace the reference immutably — never mutate in place.
- ✓Expose
.asReadonly()to hide the write methods; contained objects are not frozen.