Cours Angular · Module 2: Composants

Les lifecycle hooks - les rituels du guerrier

9 min de lecture
Dans cette leçon6

Imagine un composant chronomètre qui compte les secondes d'un entraînement. Quand doit-il commencer à compter ? Quand doit-il récupérer sur le serveur les données du guerrier qu'il doit afficher ? Et que se passe-t-il quand l'élève passe à une autre vue et que le chronomètre disparaît de l'écran ? Si personne ne l'arrête, il continue de tourner dans la mémoire du navigateur, comme le fantôme d'un guerrier qui fait tournoyer son sabre dans un dojo vide. Ce genre de bug s'appelle une fuite de mémoire.

Chaque composant Angular traverse le même cycle de vie : Angular le crée, lui transmet des données, affiche sa vue, la rafraîchit après les changements et, pour finir, le supprime. Aux moments importants de ce cycle, Angular appelle des méthodes aux noms fixes, appelées lifecycle hooks, les crochets du cycle de vie. Ce sont comme les rituels d'un samouraï : un en entrant au dojo, un autre avant le combat, un autre encore au moment de partir.

Constructeur ou ngOnInit

Le premier moment de la vie d'un composant est le constructeur. Angular l'appelle en créant l'instance de la classe, et c'est le moment de préparer l'équipement, c'est-à-dire d'injecter les dépendances, comme les services du cinquième module. À ce stade, le composant ne connaît pas encore les données de son parent. Un champ marqué du décorateur @Input(), dont le parent fournit la valeur dans son template (nous le verrons en détail dans la leçon suivante), n'a alors que sa valeur par défaut.

Les données du parent ne sont prêtes que dans ngOnInit. Angular appelle ce hook une seule fois, après avoir fixé les premières valeurs des inputs, ce qui en fait le bon endroit pour la logique de démarrage et pour la récupération de données depuis une API. Cet exemple montre la différence :

1import { Component, Input, OnInit } from '@angular/core';
2
3@Component({
4  selector: 'app-samurai',
5  standalone: true,
6  template: `<p>{{ greeting }}</p>`
7})
8export class SamuraiComponent implements OnInit {
9  @Input() name = '';
10  greeting = '';
11
12  constructor() {
13    console.log('Constructeur, name :', this.name);
14  }
15
16  ngOnInit(): void {
17    this.greeting = `Bienvenue, ${this.name} !`;
18  }
19}

Quand le parent écrit <app-samurai name="Musashi">, le constructeur affiche dans la console un nom vide, car le parent ne l'a pas encore transmis. Au moment où Angular appelle ngOnInit, le champ name contient déjà « Musashi », donc la salutation est correcte. Si tu la construisais dans le constructeur, l'élève verrait « Bienvenue, ! ». Mon conseil : réserve le constructeur aux dépendances, et déplace dans ngOnInit tout ce qui utilise les données du parent ou du serveur.

Les interfaces OnInit et OnDestroy

Dans l'exemple, la classe porte la mention implements OnInit, et OnInit figure dans l'import depuis @angular/core. L'interface OnInit est un contrat : une classe qui l'implémente promet d'avoir une méthode ngOnInit(). Tu écris le mot implements dans la déclaration de la classe, après son nom, par exemple export class DojoComponent implements OnInit {. TypeScript vérifie la promesse, comme le montre une faute de frappe dans le nom de la méthode :

1import { Component, OnInit } from '@angular/core';
2
3@Component({
4  selector: 'app-dojo',
5  standalone: true,
6  template: `<h1>{{ message }}</h1>`
7})
8export class DojoComponent implements OnInit {
9  message = '';
10
11  // faute de frappe : ngOnInt au lieu de ngOnInit
12  ngOnInt(): void {
13    this.message = 'Le dojo est ouvert !';
14  }
15}

Le compilateur rejette ce code avec un message indiquant que la classe DojoComponent implémente incorrectement l'interface OnInit, parce que la méthode ngOnInit manque. Sans la mention implements, le code compilerait sans erreur, et Angular n'appellerait tout simplement jamais la méthode mal nommée, sans le moindre avertissement, car il appelle les hooks par leur nom. Le message « Le dojo est ouvert ! » n'apparaîtrait jamais, et tu en chercherais la cause pendant des heures. Voilà pourquoi il faut toujours ajouter implements avec la bonne interface : cela coûte un mot et attrape les fautes de frappe avant qu'elles n'atteignent un élève.

Chaque hook a sa propre interface, dont le nom ne porte pas le préfixe ng : OnInit pour ngOnInit, OnDestroy pour ngOnDestroy, OnChanges pour ngOnChanges. Tu les importes toutes depuis @angular/core, et une classe peut implémenter plusieurs interfaces à la fois, séparées par des virgules : implements OnInit, OnDestroy.

ngOnChanges - quand le parent change l'ordre

Le hook ngOnChanges réagit aux changements des inputs. Angular l'appelle avant ngOnInit si le composant a des inputs liés dans le template du parent, puis à chaque changement de leurs valeurs. Le hook reçoit un objet de type SimpleChanges, dans lequel une description du changement attend sous le nom de chaque input modifié :

1import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
2
3@Component({
4  selector: 'app-health-bar',
5  standalone: true,
6  template: `<p>Santé : {{ health }}</p>`
7})
8export class HealthBarComponent implements OnChanges {
9  @Input() health = 100;
10
11  ngOnChanges(changes: SimpleChanges): void {
12    const change = changes['health'];
13    if (change && !change.firstChange) {
14      console.log(`Santé : ${change.previousValue} -> ${change.currentValue}`);
15    }
16  }
17}

La description d'un changement a trois champs : previousValue est l'ancienne valeur, currentValue la nouvelle, et firstChange vaut true au premier appel, quand le parent ne fait que fixer l'input. C'est pourquoi la condition !change.firstChange ignore le démarrage et n'affiche que les vrais changements, par exemple « Santé : 100 -> 70 » après un coup. Garde à l'esprit que ngOnChanges ne réagit qu'aux nouvelles valeurs venues du parent. Si le composant affecte lui-même this.health = 50, le hook n'est pas appelé.

ngAfterViewInit - la vue prête au combat

Après ngOnInit, Angular affiche le template du composant et ses composants enfants. Quand toute la vue est prête, il appelle ngAfterViewInit. C'est le premier moment où tu peux atteindre sans risque les éléments du template, par exemple avec le décorateur @ViewChild, qui désigne un élément marqué dans le template par un nom précédé d'un dièse, comme #nameField. Avec les réglages par défaut, un tel élément n'existe pas encore dans ngOnInit. Les usages typiques consistent à placer le curseur dans un champ de formulaire ou à lancer une bibliothèque qui dessine un graphique dans un élément prêt. Dans le sixième module, tu découvriras la version plus récente de ce décorateur, basée sur les signaux : la fonction viewChild().

ngOnDestroy - faire le ménage avant de partir

Le dernier rituel est ngOnDestroy. Angular l'appelle juste avant de supprimer le composant, par exemple quand l'élève passe à une autre vue ou quand un bloc @if retire le composant de la page. C'est l'endroit pour faire le ménage : arrêter les minuteries, annuler les abonnements et détacher les écouteurs que le composant a lui-même lancés. Voici un chronomètre qui ne laisse aucun fantôme derrière lui :

1import { Component, OnInit, OnDestroy } from '@angular/core';
2
3@Component({
4  selector: 'app-timer',
5  standalone: true,
6  template: `<p>Temps : {{ seconds }} s</p>`
7})
8export class TimerComponent implements OnInit, OnDestroy {
9  seconds = 0;
10  private intervalId?: number;
11
12  ngOnInit(): void {
13    this.intervalId = window.setInterval(() => {
14      this.seconds++;
15    }, 1000);
16  }
17
18  ngOnDestroy(): void {
19    window.clearInterval(this.intervalId);
20  }
21}

Dans ngOnInit, le chronomètre lance setInterval, qui augmente seconds chaque seconde, et mémorise son identifiant dans le champ intervalId. Dans ngOnDestroy, il passe cet identifiant à clearInterval, donc l'intervalle s'arrête en même temps que le composant. Sans cette seule ligne, chaque visite de la vue lancerait un nouveau chronomètre invisible, et l'application ralentirait de minute en minute. Les abonnements aux flux RxJS, que tu découvriras dans le septième module, se nettoient au même endroit.

Angular moderne propose aussi un chemin plus court. La fonction inject() obtient une dépendance directement dans un champ de la classe, et l'objet DestroyRef permet d'écrire le nettoyage juste à côté du code qui lance quelque chose :

1import { Component, DestroyRef, OnInit, inject } from '@angular/core';
2
3@Component({
4  selector: 'app-timer',
5  standalone: true,
6  template: `<p>Temps : {{ seconds }} s</p>`
7})
8export class TimerComponent implements OnInit {
9  seconds = 0;
10  private destroyRef = inject(DestroyRef);
11
12  ngOnInit(): void {
13    const id = window.setInterval(() => this.seconds++, 1000);
14    this.destroyRef.onDestroy(() => window.clearInterval(id));
15  }
16}

L'effet est le même : l'intervalle s'arrête au moment où le composant est supprimé. La différence, c'est que le démarrage et le nettoyage se trouvent côte à côte, ce qui rend l'oubli du second plus difficile. Les deux chemins sont corrects, et dans les exercices de ce module, tu pratiques le ngOnDestroy classique.

L'ordre des appels

Angular appelle toujours les hooks dans le même ordre. Voici la liste complète, de la naissance au départ :

  1. constructor - création de l'instance de la classe (une fois)
  2. ngOnChanges - avant ngOnInit s'il y a des inputs liés dans le template, puis après chacun de leurs changements (plusieurs fois)
  3. ngOnInit - initialisation du composant (une fois)
  4. ngDoCheck - vérification des changements personnalisée (plusieurs fois)
  5. ngAfterContentInit - après l'insertion du contenu transmis par <ng-content> (une fois)
  6. ngAfterContentChecked - après la vérification de ce contenu (plusieurs fois)
  7. ngAfterViewInit - après l'affichage de la vue et des enfants (une fois)
  8. ngAfterViewChecked - après la vérification de la vue (plusieurs fois)
  9. ngOnDestroy - juste avant la suppression du composant (une fois)

Tu n'es pas obligé de tous les implémenter. En pratique, tu en utiliseras surtout quatre : ngOnChanges quand tu réagis à de nouvelles données du parent, ngOnInit pour le démarrage, ngAfterViewInit quand tu as besoin d'une vue prête, et ngOnDestroy pour le nettoyage. Le constructeur vient toujours en premier, et ngOnDestroy toujours en dernier. Angular appelle les hooks qui se terminent par Checked ainsi que ngDoCheck à chaque vérification des changements, c'est-à-dire très souvent, alors n'y place pas de calculs lourds. Tu approfondiras la récupération de données dans ngOnInit au neuvième module, avec la communication avec une API.

Retiens bien : le constructeur prépare l'équipement, ngOnInit lance le combat, et ngOnDestroy nettoie le champ de bataille avant que le guerrier ne s'en aille.

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quel lifecycle hook est idéal pour récupérer des données depuis une API à l'initialisation du composant ?

  2. 2. Quel lifecycle hook est utilisé pour nettoyer les ressources (désabonnements, arrêt des timers) avant la destruction du composant ?

  3. 3. Quel lifecycle hook est appelé à chaque changement d'une valeur @Input ?

Tâches pratiques dans le jeu

  • Tri vertical

    Ordonne les lifecycle hooks dans l'ordre où Angular les appelle (du premier au dernier) :

  • Éditeur de code

    Implémente les interfaces OnInit et OnDestroy, ainsi que les méthodes de lifecycle hooks correspondantes.

  • Tri horizontal

    Ordonne les éléments sur une seule ligne d'une déclaration de classe implémentant le hook OnInit :

Articles utiles