Angular Signals : guide complet

🏷️ Front-end 📅 10/04/2026 09:00:00 👤 Mezgani said
Angular Signals Computed State Management
Angular Signals : guide complet

Comprendre et utiliser les Signals Angular pour gérer l'état de façon réactive et performante sans NgRx.

Prérequis et version Angular

Les Signals sont disponibles à partir d'Angular 16 (mode preview) et sont stables depuis Angular 17. Aucune installation de librairie tierce n'est nécessaire, tout est inclus dans @angular/core.

Version minimale: Angular 16 pour tester, Angular 17+ pour la production. Vérifie ta version avec ng version.

Pour créer un nouveau projet Angular avec les Signals activés par défaut:

npm install -g @angular/cli
ng new mon-projet-signals
cd mon-projet-signals
ng serve

Les imports nécessaires viennent directement de @angular/core — pas de module à ajouter.

Créer et modifier un signal

Un signal est une valeur réactive qui notifie automatiquement les dépendances quand elle change. On le crée avec la fonction signal().

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

// Créer un signal avec une valeur initiale
const compteur = signal(0);

// Lire la valeur (appel comme une fonction)
console.info(compteur()); // 0

// Remplacer la valeur
compteur.set(5);
console.info(compteur()); // 5

// Modifier à partir de la valeur actuelle
compteur.update(valeur => valeur + 1);
console.info(compteur()); // 6

set() vs update()

  1. set(valeur) — remplace complètement la valeur.
  2. update(fn) — calcule la nouvelle valeur à partir de l'ancienne.

Pour les signaux de type objet ou tableau, utilise update() en retournant une nouvelle référence:

const utilisateur = signal({ nom: 'Alice', age: 30 });

// Retourner un nouvel objet (immutabilité)
utilisateur.update(u => ({ ...u, age: 31 }));

Signaux dérivés avec computed()

computed() crée un signal en lecture seule dont la valeur est calculée automatiquement à partir d'autres signals. Il se met à jour uniquement quand ses dépendances changent.

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

const prix = signal(100);
const quantite = signal(3);

// Se recalcule si prix ou quantite changent
const total = computed(() => prix() * quantite());

console.infoo(total()); // 300

prix.set(150);
console.info(total()); // 450
A retenir: un signal computed() est en lecture seule. On ne peut pas appeler .set() dessus. C'est une valeur dérivée, pas une source.

On peut chaîner plusieurs computed() — Angular ne recalcule que ce qui a réellement changé:

const prixHT = signal(80);
const tva = signal(0.2);
const prixTTC = computed(() => prixHT() * (1 + tva()));
const label = computed(() => `Prix TTC: ${prixTTC().toFixed(2)} €`);

Effets de bord avec effect()

effect() exécute une fonction chaque fois que les signals qu'elle lit changent. C'est l'équivalent de subscribe() pour les Observables, sans gestion manuelle de la désinscription.

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

@Component({ ... })
export class MonComposant {
    readonly theme = signal('light');

    constructor() {
        // S'exécute immédiatement, puis à chaque changement de theme
        effect(() => {
            document.body.setAttribute('data-theme', this.theme());
            console.info('Thème actif:', this.theme());
        });
    }
}

Règles d'utilisation de effect()

  1. Doit être créé dans un contexte d'injection (constructeur ou inject()).
  2. Ne pas modifier un signal depuis un effect() — risque de boucle infinie.
  3. Angular détruit automatiquement l'effet quand le composant est détruit.

Exemple complet dans un composant

Un composant compteur standalone Angular 17+ avec signal, computed et effect:

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

@Component({
    selector: 'app-compteur',
    standalone: true,
    template: `
        <h2>Compteur: {{ compteur() }}</h2>
        <p>{{ message() }}</p>
        <button (click)="incrementer()">+1</button>
        <button (click)="reinitialiser()">Reset</button>
    `
})
export class CompteurComponent {
    readonly compteur = signal(0);

    readonly message = computed(() =>
        this.compteur() >= 10
            ? 'Limite atteinte !'
            : `${10 - this.compteur()} clics restants`
    );

    constructor() {
        effect(() => console.info('Valeur:', this.compteur()));
    }

    incrementer() {
        this.compteur.update(v => Math.min(v + 1, 10));
    }

    reinitialiser() {
        this.compteur.set(0);
    }
}
Dans le template: on lit un signal en l'appelant comme une fonction — {{ compteur() }}. Angular détecte les dépendances et ne re-rend que ce qui est nécessaire.

Bonnes pratiques production

  • Déclarer les signals readonly dans les composants pour éviter les mutations externes.
  • Préférer computed() à effect() pour les valeurs dérivées — meilleure performance.
  • Ne jamais modifier un signal dans un effect() sans raison valable.
  • Pour les flux async (HTTP, WebSocket), utiliser toSignal() de @angular/core/rxjs-interop.
  • Signals et RxJS coexistent sans problème — migrer progressivement.

Convertir un Observable en Signal avec toSignal():

import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { inject, Component } from '@angular/core';

@Component({ ... })
export class ArticlesComponent {
    private http = inject(HttpClient);

    // Observable converti en Signal — pas d'async pipe, pas de subscribe
    readonly articles = toSignal(
        this.http.get<Article[]>('/api/articles'),
        { initialValue: [] }
    );
}