Le signal writable : lecture et écriture

À retenir — On lit un signal en l'appelant count(), on remplace sa valeur avec count.set(x) et on la dérive de l'ancienne avec count.update(n => n + 1). Le template se met à jour tout seul.

Le signal writable est la brique de base. C'est une valeur que vous pouvez lire et modifier, et à laquelle l'interface réagit automatiquement. Ce chapitre couvre sa création, ses trois opérations (set, update, lecture) et son affichage dans un composant.

Comment créer et lire un signal ?

On crée un signal avec la fonction signal(), en lui passant une valeur initiale. Le type est inféré, mais on peut le préciser :

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

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

Pour lire la valeur, on appelle le signal comme une fonction :

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

Cet appel count() n'est pas qu'une lecture : il enregistre une dépendance. Si la lecture a lieu dans un template ou un computed, ce consommateur sera notifié au prochain changement.

set() ou update() : quelle différence ?

Il existe deux manières d'écrire dans un signal writable.

set(valeur) remplace la valeur, sans tenir compte de l'ancienne :

count.set(10); // la valeur devient 10

update(fn) calcule la nouvelle valeur à partir de l'ancienne :

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

Règle pratique : utilisez set quand la nouvelle valeur est indépendante de l'actuelle (réinitialiser à 0, affecter une saisie), et update quand elle en dépend (incrémenter, basculer un booléen, ajouter à une liste).

actif.update((v) => !v);                 // bascule true/false
liste.update((arr) => [...arr, nouvel]); // ajoute sans muter l'ancien tableau

L'immuabilité, point de vigilance

Un signal ne détecte un changement que si on lui fournit une nouvelle référence. Muter en place ne déclenche aucune mise à jour :

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

// ❌ NE MARCHE PAS : on mute le tableau existant, la référence ne change pas.
panier().push('thé');

// ✅ CORRECT : on crée un nouveau tableau.
panier.update((items) => [...items, 'thé']);

La même règle vaut pour les objets : on remplace via { ...ancien, champ: valeur } plutôt que d'assigner une propriété.

Lire un signal dans un template

Dans un composant, on appelle simplement le signal dans le template. Angular suit la dépendance et rafraîchit l'affichage à chaque changement — y compris avec la stratégie OnPush, fortement recommandée avec les signals.

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

@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <h1>Compteur : {{ count() }}</h1>
    <button (click)="decrement()">−</button>
    <button (click)="increment()">+</button>
    <button (click)="reset()">Réinitialiser</button>
    <p>Valeur doublée (calculée dans le 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);
  }
}

Notez le readonly sur la propriété : la référence du signal ne change jamais (c'est sa valeur interne qui évolue), donc on protège le champ contre une réassignation accidentelle.

Essayez en direct

Modifiez le code ci-dessous (par exemple, faites incrémenter de 5 en 5) et observez la mise à jour instantanée :

signal-basics

signal en lecture seule : asReadonly()

Pour exposer un signal sans permettre l'écriture depuis l'extérieur (utile pour un service), on utilise asReadonly() :

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

increment(): void {
  this._count.update((n) => n + 1); // l'écriture reste interne
}

On retrouvera ce patron au chapitre 9 pour construire un service de state propre.

Égalité et changements ignorés

Par défaut, un signal compare l'ancienne et la nouvelle valeur avec Object.is. Si elles sont identiques, aucun consommateur n'est notifié :

const x = signal(3);
x.set(3); // même valeur → aucun rafraîchissement déclenché

Pour les objets, on peut fournir une fonction d'égalité personnalisée :

const user = signal(
  { id: 1, nom: 'Ada' },
  { equal: (a, b) => a.id === b.id }, // considère « égal » si même id
);

En résumé

  • signal(valeur) crée une source réactive ; on la lit en l'appelant.
  • set remplace, update dérive de l'ancienne valeur.
  • Toujours fournir une nouvelle référence pour les tableaux et objets.
  • asReadonly() protège l'écriture ; l'option equal contrôle la détection.

Au chapitre suivant, on passe aux valeurs dérivées avec computed() : comment calculer automatiquement un total, un filtre ou un libellé à partir d'autres signals.

Nous utilisons Microsoft Clarity pour comprendre comment le site est utilisé et l'améliorer. En poursuivant votre navigation, vous l'acceptez. Vous pouvez le désactiver à tout moment.