Creating & Reading Signals
A writable signal is the most fundamental reactive primitive in Angular. It wraps a single value, lets any code read that value, and notifies every dependent consumer whenever the value changes. Understanding how to create writable signals and how to read and mutate them is the foundation for everything else in the signals world — computed values, effects, and the modern component APIs all build on top of this one piece.
Creating a writable signal
You create a writable signal by calling the signal() factory imported from @angular/core and passing it an initial value. The type is inferred from that value, though you can supply an explicit generic when you need a wider type (for example, a value that starts as null but will later hold an object).
import { signal, WritableSignal } from '@angular/core';
const count = signal(0); // WritableSignal<number>
const name = signal('Ada'); // WritableSignal<string>
const user = signal<User | null>(null); // explicit type
interface User {
id: number;
name: string;
}
The returned value is a WritableSignal<T>. It is callable — calling it reads the value — and it also carries the set, update, and asReadonly methods used to change or expose the value.
Reading a signal
To read a signal you call it like a function. There is no .value property and no subscription to manage; the call itself returns the current value.
const count = signal(10);
console.log(count()); // read the value
console.log(count() * 2); // use it in an expression
Output:
10
20
Reading a signal inside a reactive context — a computed, an effect, or a component template — registers a dependency. When the signal later changes, Angular re-runs only the consumers that actually read it. In a template you simply invoke the signal in the binding expression:
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
template: `
<p>Current count: {{ count() }}</p>
@if (count() > 5) {
<p>That's a lot!</p>
}
<button (click)="increment()">Add</button>
`,
})
export class CounterComponent {
protected readonly count = signal(0);
increment(): void {
this.count.update((n) => n + 1);
}
}
Tip: Always call the signal —
count()— in templates and code. Referencing it without parentheses (count) yields the signal function object itself, not its value, which is a common source of confusing bugs.
Updating with set()
Use set() when you want to replace the value outright, independent of what it was before. This is ideal for assignments such as storing a fresh value from a form or an API response.
const status = signal('idle');
status.set('loading');
status.set('ready');
console.log(status()); // 'ready'
Updating with update()
Use update() when the new value is derived from the current one. It receives a callback whose argument is the present value and whose return value becomes the new value. This avoids the read-then-set race of computing the next state separately.
const count = signal(0);
count.update((n) => n + 1); // 1
count.update((n) => n * 10); // 10
console.log(count()); // 10
Warning: Signals use referential equality (
Object.is) by default to decide whether a value actually changed. Mutating an object or array in place and then passing the same reference will NOT trigger updates. Always produce a new reference.
const todos = signal<string[]>(['Buy milk']);
// Wrong — same array reference, no notification
todos().push('Walk dog');
// Right — new array, dependents re-run
todos.update((list) => [...list, 'Walk dog']);
set() vs update()
| Method | When to use | Callback receives current value |
|---|---|---|
set(value) | Replace the value with one you already have | No |
update(fn) | Derive the next value from the current value | Yes |
Both methods schedule the same change notification; choosing between them is purely a matter of whether the new value depends on the old one.
Exposing a read-only view
Sometimes a component owns a writable signal but wants to expose it to children or templates without letting them mutate it. Call asReadonly() to get a Signal<T> that can be read but not written.
import { signal } from '@angular/core';
export class CartService {
private readonly _items = signal<string[]>([]);
// Public, read-only surface
readonly items = this._items.asReadonly();
add(item: string): void {
this._items.update((list) => [...list, item]);
}
}
Consumers read cart.items() freely, but only CartService can change the underlying value.
Best Practices
- Mark signal fields
readonly— the binding never changes, only the value inside it does. - Always create a new object or array reference when updating; never mutate in place.
- Reach for
update()whenever the next value depends on the current one, andset()only for outright replacement. - Expose internal writable signals through
asReadonly()so callers cannot bypass your mutation methods. - Keep signals holding plain state; derive everything else with
computed()rather than storing redundant copies. - Provide an explicit generic (for example
signal<User | null>(null)) when the initial value is narrower than the intended type.