Les Lifecycle hooks (ngOnInit, ngOnChanges, ngOnDestroy...) existent toujours et fonctionnent. Mais avec les Signals, la plupart deviennent inutiles : dériver depuis les inputs se fait avec computed(), réagir avec effect(), travailler sur le DOM avec afterNextRender(), et nettoyer avec DestroyRef.
Le tableau d’équivalence
Ancien hook → Équivalent moderne
───────────────────────────────────────────────────────────────────
constructor → constructor (DI, Signals, effect, afterNextRender)
ngOnChanges → computed() / effect() / linkedSignal() sur les inputs
ngOnInit → computed() / resource() (ou ngOnInit, toujours valide)
ngDoCheck → vous n’en avez presque jamais besoin
ngAfterContentInit → contentChild() / contentChildren() (Signals)
ngAfterViewInit → afterNextRender() + viewChild()
ngAfterViewChecked → afterEveryRender() (rarement)
ngOnDestroy → inject(DestroyRef).onDestroy() / takeUntilDestroyed()À quoi ressemble un composant moderne
import { Component, DestroyRef, afterNextRender, computed, effect, inject, input } from '@angular/core';
@Component({ selector: 'app-user-panel', /* ... */ })
export class UserPanel {
readonly userId = input.required<number>();
private readonly api = inject(UserApi); // DI : dans les champs / le constructeur
// « ngOnChanges » → computed / effect / resource sur les inputs
readonly user = httpResource<User>(() => `/api/users/${this.userId()}`);
readonly title = computed(() => this.user.value()?.name ?? 'Chargement…');
constructor() {
// « ngAfterViewInit » → afterNextRender (une fois, après le premier rendu)
afterNextRender(() => this.chart.init(this.canvas().nativeElement));
// « ngOnDestroy » → DestroyRef.onDestroy (fonctionne aussi dans les services, fonctions, directives)
inject(DestroyRef).onDestroy(() => this.chart.dispose());
// réaction au changement d’un input
effect(() => this.analytics.track('user-panel-open', { id: this.userId() }));
}
}- Cliquez sur « Créer le Child ».
ngOnChanges avec SimpleChanges typé v21
// Besoin de ngOnChanges malgré tout ? Depuis la v21, SimpleChanges est générique :
export class Legacy implements OnChanges {
readonly name = input('');
readonly age = input(0);
ngOnChanges(changes: SimpleChanges<Legacy>) {
if (changes.name) { // typé, sans changes['name']
console.log(changes.name.previousValue, '→', changes.name.currentValue);
}
}
}DestroyRef : un nettoyage réutilisable
Contrairement à ngOnDestroy (qui n’existe que sur les classes), DestroyRef peut être injecté n’importe où, y compris dans des fonctions. Cela permet des « hooks » réutilisables qui se nettoient tout seuls.
// DestroyRef fonctionne partout où il y a un contexte d’injection — idéal pour des fonctions réutilisables
export function useInterval(ms: number, fn: () => void): void {
const id = setInterval(fn, ms);
inject(DestroyRef).onDestroy(() => clearInterval(id));
}
export class Clock {
readonly now = signal(new Date());
constructor() {
useInterval(1000, () => this.now.set(new Date())); // se nettoie tout seul
}
}afterNextRender / afterEveryRender
// afterNextRender — une fois ; afterEveryRender — après chaque rendu de l’application (rarement nécessaire)
afterNextRender({
read: () => this.el.nativeElement.offsetHeight, // lit la mise en page
// write: (height) => ... // puis écrit (phases séparées = pas de thrashing)
});
// S’exécute uniquement dans le navigateur (pas pendant le SSR) : le bon endroit pour window, document, localStorage#1 Le build est passé ?
#2 Oui, et tous les tests sont verts.
#3 Alors j’ouvre la PR.
#4 Préviens-moi quand elle est mergée.
send() {
this.messages.update((list) => [...list, next]);
const el = this.chat().nativeElement;
el.scrollTop = el.scrollHeight; // le DOM a encore l’ancienne liste
}Le nettoyage des abonnements, timers et listeners est détaillé dans Fuites mémoire : les trouver et les corriger ; le code qui touche window ou document doit aussi tourner sur le serveur : Code compatible serveur.
Quand OUI, quand NON
- Du code existant qui fonctionne : inutile de forcer une réécriture.
ngOnInitpour une initialisation qui lit les inputs une seule fois (il reste valide et lisible).- Interfaces exigées par des bibliothèques (p. ex. le
ControlValueAccessorclassique).
- Recalculer une valeur quand un input change →
computed(). - Réinitialiser un état local quand un input change →
linkedSignal(). - Charger des données quand un input change →
httpResource(). - Se désabonner dans
ngOnDestroy→takeUntilDestroyed()/toSignal.
Pièges fréquents
- Les inputs dans le constructeur : ils ne sont pas définis. Lire un
input.requiredà cet endroit lève une erreur. window/documentdans le constructeur avec SSR → une erreur côté serveur. Déplacez le code dansafterNextRender.ngOnChangesne se déclenche pas pour les mutations internes des objets reçus, seulement pour les nouvelles références.