The writable signal: reading and writing

Key takeaway — You read a signal by calling it count(), you replace its value with count.set(x), and you derive it from the previous one with count.update(n => n + 1). The template updates on its own.

The writable signal is the building block. It is a value you can read and modify, and that the interface reacts to automatically. This chapter covers its creation, its three operations (set, update, reading) and its display in a component.

How do you create and read a signal?

You create a signal with the signal() function, passing it an initial value. The type is inferred, but you can specify it:

import { signal } from '@angular/core';

const count = signal(0);              // WritableSignal<number>
const nom = signal<string>('Ada');    // explicit type
const actif = signal(false);          // WritableSignal<boolean>

To read the value, you call the signal like a function:

console.log(count()); // 0
console.log(nom());   // 'Ada'

This count() call is not just a read: it registers a dependency. If the read happens inside a template or a computed, that consumer will be notified on the next change.

set() or update(): what is the difference?

There are two ways to write to a writable signal.

set(value) replaces the value, regardless of the previous one:

count.set(10); // the value becomes 10

update(fn) computes the new value from the previous one:

count.update((n) => n + 1); // increments: 10 → 11

Rule of thumb: use set when the new value is independent of the current one (resetting to 0, assigning an input), and update when it depends on it (incrementing, toggling a boolean, adding to a list).

actif.update((v) => !v);                 // toggles true/false
liste.update((arr) => [...arr, nouvel]); // adds without mutating the old array

Immutability, a point to watch

A signal only detects a change if you provide it a new reference. Mutating in place triggers no update:

const panier = signal<string[]>(['café']);

// ❌ DOES NOT WORK: we mutate the existing array, the reference does not change.
panier().push('thé');

// ✅ CORRECT: we create a new array.
panier.update((items) => [...items, 'thé']);

The same rule applies to objects: replace via { ...ancien, champ: valeur } rather than assigning a property.

Reading a signal in a template

In a component, you simply call the signal in the template. Angular tracks the dependency and refreshes the display on every change — including with the OnPush strategy, strongly recommended with signals.

import { Component, signal, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <h1>Counter: {{ count() }}</h1>
    <button (click)="decrement()">−</button>
    <button (click)="increment()">+</button>
    <button (click)="reset()">Reset</button>
    <p>Doubled value (computed in the template): {{ count() * 2 }}</p>
  `,
})
export class AppComponent {
  readonly count = signal(0);

  increment(): void {
    this.count.update((n) => n + 1);
  }

  decrement(): void {
    this.count.update((n) => n - 1);
  }

  reset(): void {
    this.count.set(0);
  }
}

Note the readonly on the property: the signal's reference never changes (it is its internal value that evolves), so we protect the field against accidental reassignment.

Try it live

Edit the code below (for example, make it increment by 5) and watch the instant update:

signal-basics

Read-only signal: asReadonly()

To expose a signal without allowing writes from the outside (useful for a service), use asReadonly():

private readonly _count = signal(0);
readonly count = this._count.asReadonly(); // Signal<number>, without set/update

increment(): void {
  this._count.update((n) => n + 1); // writing stays internal
}

We will see this pattern again in chapter 9 to build a clean state service.

Equality and ignored changes

By default, a signal compares the old and new values with Object.is. If they are identical, no consumer is notified:

const x = signal(3);
x.set(3); // same value → no refresh triggered

For objects, you can provide a custom equality function:

const user = signal(
  { id: 1, nom: 'Ada' },
  { equal: (a, b) => a.id === b.id }, // treats as "equal" if same id
);

Key points

  • signal(value) creates a reactive source; you read it by calling it.
  • set replaces, update derives from the previous value.
  • Always provide a new reference for arrays and objects.
  • asReadonly() protects against writes; the equal option controls detection.

In the next chapter, we move on to derived values with computed(): how to automatically compute a total, a filter or a label from other signals.

We use Microsoft Clarity to understand how the site is used and improve it. By continuing to browse, you accept it. You can disable it at any time.