Control flow, OnPush et mode zoneless

À retenir — Le nouveau control flow (@if, @for, @switch) se marie nativement aux signals. Avec OnPush et, à terme, le mode zoneless, Angular ne rafraîchit que les vues dont un signal a réellement changé.

Les signals prennent tout leur sens au moment du rendu. Ce chapitre relie l'état réactif au template moderne d'Angular et explique comment obtenir une détection de changement minimale et performante.

Le nouveau control flow

Depuis Angular 17, les directives *ngIf et *ngFor cèdent la place à une syntaxe intégrée au template, plus lisible et plus rapide. Elle lit les signals directement.

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    @if (taches().length === 0) {
      <p>Aucune tâche. 🎉</p>
    } @else {
      <ul>
        @for (tache of taches(); track tache.id) {
          <li>{{ tache.texte }}</li>
        } @empty {
          <li>Liste vide</li>
        }
      </ul>
    }
  `,
})

Points clés :

  • @for exige track. L'expression de suivi (souvent track item.id) permet à Angular de réutiliser les nœuds du DOM au lieu de tout recréer. C'est obligatoire et crucial pour la performance.
  • @empty gère le cas d'une liste vide, sans @if supplémentaire.
  • @switch remplace [ngSwitch] :
@switch (statut()) {
  @case ('chargement') { <app-spinner /> }
  @case ('erreur')     { <p>Une erreur est survenue.</p> }
  @default             { <app-liste [donnees]="donnees()" /> }
}

Exemple complet : liste de tâches réactive

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

interface Tache {
  id: number;
  texte: string;
  faite: boolean;
}

@Component({
  selector: 'app-root',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <h1>Tâches</h1>
    <input
      #champ
      (keyup.enter)="ajouter(champ.value); champ.value = ''"
      placeholder="Nouvelle tâche puis Entrée…"
    />

    @if (taches().length === 0) {
      <p>Aucune tâche pour l'instant. 🎉</p>
    } @else {
      <ul>
        @for (tache of taches(); track tache.id) {
          <li>
            <label>
              <input
                type="checkbox"
                [checked]="tache.faite"
                (change)="basculer(tache.id)"
              />
              {{ tache.texte }}
            </label>
          </li>
        }
      </ul>
      <p>{{ restantes() }} tâche(s) restante(s)</p>
    }
  `,
})
export class AppComponent {
  private prochainId = 1;

  readonly taches = signal<Tache[]>([]);
  readonly restantes = computed(() => this.taches().filter((t) => !t.faite).length);

  ajouter(texte: string): void {
    const valeur = texte.trim();
    if (!valeur) return;
    this.taches.update((liste) => [
      ...liste,
      { id: this.prochainId++, texte: valeur, faite: false },
    ]);
  }

  basculer(id: number): void {
    this.taches.update((liste) =>
      liste.map((t) => (t.id === id ? { ...t, faite: !t.faite } : t)),
    );
  }
}

Essayez en direct

control-flow-taches

Pourquoi associer signals et OnPush ?

La stratégie ChangeDetectionStrategy.OnPush indique à Angular de ne pas revérifier un composant à chaque cycle global, mais seulement quand c'est nécessaire. Or les signals notifient précisément la vue concernée à chaque changement. La combinaison est idéale :

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  // …
})

Avec des signals dans le template, OnPush n'introduit aucun risque de vue obsolète : tout changement de signal lu par la vue déclenche son rafraîchissement. C'est pourquoi toutes les démos de cette formation utilisent OnPush. Prenez-en l'habitude dès maintenant.

Le mode zoneless

Historiquement, Angular détecte les changements grâce à Zone.js, qui « patche » les API du navigateur (timers, événements, fetch) pour déclencher la détection. C'est pratique mais global et coûteux.

Comme les signals signalent eux-mêmes leurs changements, Angular peut se passer de Zone.js : c'est le mode zoneless (stabilisé dans Angular 20, disponible en preview avant). On l'active au bootstrap :

import { bootstrapApplication } from '@angular/platform-browser';
import { provideZonelessChangeDetection } from '@angular/core';

bootstrapApplication(AppComponent, {
  providers: [provideZonelessChangeDetection()],
});

Avantages : bundle plus léger (on supprime zone.js), rendu plus prévisible et souvent plus rapide. Condition : votre réactivité doit passer par des signals (ou des notifications explicites) plutôt que par des mutations « magiques » détectées par Zone.

En zoneless, un état muté hors signal (par exemple une propriété de classe modifiée dans un setTimeout) ne rafraîchit plus la vue automatiquement. Migrer vers les signals devient alors la bonne pratique par défaut.

Performance : les bons réflexes

  • track pertinent dans chaque @for (un identifiant stable, jamais l'index si la liste change d'ordre).
  • computed plutôt que recalculer dans le template (un calcul lourd inline serait ré-évalué à chaque rendu).
  • OnPush partout : avec les signals, c'est gratuit en sécurité.
  • Découper les gros composants : plus les vues sont petites, plus les mises à jour sont ciblées.

En résumé

  • @if / @for (avec track) / @switch / @empty : control flow moderne qui lit les signals.
  • OnPush + signals = rafraîchissements ciblés, sans vue obsolète.
  • Le mode zoneless supprime Zone.js et s'appuie sur les signals.
  • track, computed et le découpage sont les leviers de performance.

Dernier chapitre : assembler tout cela dans une architecture de state propre — service à signals, linkedSignal, et bonnes pratiques de production.

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.