Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Angular

Le cycle de vie d’un composant Angular

Angular crée, met à jour puis détruit chaque composant, et appelle ses méthodes du cycle de vie à chaque étape. Observez leur ordre exact dans la console, apprenez à choisir le bon moment pour chaque ...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À propos de ce cours

Angular crée, met à jour puis détruit chaque composant, et appelle ses méthodes du cycle de vie à chaque étape. Observez leur ordre exact dans la console, apprenez à choisir le bon moment pour chaque traitement et à éviter les fuites, avec un exemple complet et six activités.
  1. Objectifs

    • Comprendre les trois grandes étapes de la vie d'un composant : création, mise à jour et destruction
    • Connaître les méthodes du cycle de vie (lifecycle hooks), leur ordre d'appel et l'interface correspondante
    • Choisir le bon moment pour chaque traitement : constructor ou ngOnInit, ngOnInit ou ngAfterViewInit
    • Réagir au changement d'un input avec ngOnChanges et SimpleChanges
    • Libérer les ressources dans ngOnDestroy pour éviter les fuites
    • Connaître les fonctions récentes qui complètent les hooks : effect, afterNextRender, DestroyRef
  2. Présentation

    • Un composant Angular n'existe pas en permanence : Angular le crée quand son sélecteur apparaît dans un template, le met à jour quand ses données changent, et le détruit quand il disparaît de l'écran (par exemple à la fin d'un bloc @if ou lors d'un changement de page).
    • Cette suite d'étapes s'appelle le cycle de vie du composant. À certains moments précis, Angular appelle des méthodes de la classe si elles existent : ce sont les hooks (« crochets ») du cycle de vie. Ils permettent d'exécuter son propre code au bon moment :
      • charger des données quand le composant démarre ;
      • réagir quand le parent lui envoie une nouvelle valeur ;
      • manipuler un élément du DOM une fois qu'il est affiché ;
      • arrêter un minuteur ou une connexion quand le composant est détruit.
    • Chaque hook correspond à une interface de @angular/core, dont le nom est celui de la méthode sans le préfixe ng: ngOnInit() ↔ OnInit, ngOnDestroy() ↔ OnDestroy…

Schéma du cycle de vie d'un composant Angular : création, mise à jour et destruction

  1. Les méthodes du cycle de vie

Méthode Interface Appelée… Usage typique
constructor() — une fois, à la création de l'objet, avant la réception des inputs récupérer des services avec inject(), déclarer des effect
ngOnChanges(changes) OnChanges avant ngOnInit, puis chaque fois qu'un input change réagir à une nouvelle valeur envoyée par le parent
ngOnInit() OnInit une fois, quand les inputs ont leur première valeur initialiser le composant, charger des données, démarrer un minuteur
ngDoCheck() DoCheck à chaque vérification du composant détection de changement personnalisée (rare)
ngAfterContentInit() AfterContentInit une fois, quand le contenu projeté (<ng-content>) est prêt lire un élément fourni par le parent (contentChild)
ngAfterContentChecked() AfterContentChecked après chaque vérification du contenu projeté rare
ngAfterViewInit() AfterViewInit une fois, quand la vue du composant et de ses enfants est prête lire un élément du template (viewChild), intégrer une bibliothèque JavaScript
ngAfterViewChecked() AfterViewChecked après chaque vérification de la vue rare
ngOnDestroy() OnDestroy une fois, juste avant la destruction arrêter minuteurs et abonnements, libérer les ressources
  • Les trois indispensables : dans la pratique, on utilise surtout ngOnInit, ngOnDestroy et, plus rarement, ngOnChanges et ngAfterViewInit. Les méthodes « Checked » et ngDoCheck sont appelées très souvent : un code lent à cet endroit ralentit toute l'application.
  • Le projet de démonstration

    • Pour voir le cycle de vie, on crée un composant CarteSession qui implémente les huit hooks et écrit un message dans la console à chaque appel. Le composant parent App permet de l'afficher, de le masquer et de modifier ses inputs.
    ng new cycle-de-vie --style=css --ssr=false
    cd cycle-de-vie
    ng g c carte-session
    // src/app/app.ts
    import { Component, signal } from '@angular/core';
    import { CarteSession } from './carte-session/carte-session';
    
    @Component({
      selector: 'app-root',
      imports: [CarteSession],
      styleUrl: './app.css',
      templateUrl: './app.html',
    })
    export class App {
      protected afficher = signal(true);
      protected titre = signal('Développement Angular');
      protected places = signal(12);
    
      basculer(): void {
        this.afficher.update((a) => !a);
      }
    
      reserver(): void {
        this.places.update((p) => p - 1);
      }
    
      renommer(): void {
        this.titre.set('Angular avancé');
      }
    }
    <!-- src/app/app.html -->
    <h1>Cycle de vie d'un composant</h1>
    <div class="actions">
      <button type="button" (click)="basculer()">Afficher / masquer</button>
      <button type="button" (click)="reserver()">Réserver une place</button>
      <button type="button" (click)="renommer()">Changer le titre</button>
    </div>
    
    @if (afficher()) {
      <app-carte-session [titre]="titre()" [places]="places()">
        <p #note>Session de novembre – salle B12</p>
      </app-carte-session>
    } @else {
      <p>La carte est masquée.</p>
    }
    /* src/app/app.css */
    :host { display: block; padding: 16px 24px; }
    h1 { font-size: 1.4rem; margin: 0 0 12px; }
    .actions { display: flex; gap: 8px; margin-bottom: 14px; }
    button {
      background: #1e293b; color: #fff; border: none;
      border-radius: 6px; padding: 7px 12px; cursor: pointer;
    }
    • Le composant CarteSession reçoit deux inputs (titre et places), affiche depuis combien de secondes il existe (un minuteur démarré dans ngOnInit et arrêté dans ngOnDestroy), et projette le paragraphe fourni par le parent avec <ng-content />:
    // src/app/carte-session/carte-session.ts
    import {
      AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit,
      Component, DoCheck, ElementRef, OnChanges, OnDestroy, OnInit,
      SimpleChanges, contentChild, input, signal, viewChild,
    } from '@angular/core';
    
    let compteur = 0;
    function journal(message: string): void {
      compteur++;
      console.log(`${String(compteur).padStart(2, '0')}. ${message}`);
    }
    
    @Component({
      selector: 'app-carte-session',
      styleUrl: './carte-session.css',
      templateUrl: './carte-session.html',
    })
    export class CarteSession
      implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked,
        AfterViewInit, AfterViewChecked, OnDestroy
    {
      titre = input.required<string>();
      places = input(0);
    
      protected secondes = signal(0);
      private minuteur?: ReturnType<typeof setInterval>;
    
      private titreEl = viewChild<ElementRef<HTMLHeadingElement>>('titreEl');
      private note = contentChild<ElementRef<HTMLElement>>('note');
    
      constructor() {
        journal('constructor');
      }
    
      ngOnChanges(changes: SimpleChanges): void {
        for (const nom in changes) {
          const c = changes[nom];
          journal(`ngOnChanges : ${nom} ${JSON.stringify(c.previousValue)} → ${JSON.stringify(c.currentValue)}`);
        }
      }
    
      ngOnInit(): void {
        journal(`ngOnInit : titre = « ${this.titre()} », texte du <h3> = « ${this.titreEl()?.nativeElement.textContent} »`);
        this.minuteur = setInterval(() => this.secondes.update((s) => s + 1), 1000);
      }
    
      ngDoCheck(): void {
        journal('ngDoCheck');
      }
    
      ngAfterContentInit(): void {
        journal(`ngAfterContentInit : contenu projeté = « ${this.note()?.nativeElement.textContent} »`);
      }
    
      ngAfterContentChecked(): void {
        journal('ngAfterContentChecked');
      }
    
      ngAfterViewInit(): void {
        journal(`ngAfterViewInit : <h3> = « ${this.titreEl()?.nativeElement.textContent} »`);
      }
    
      ngAfterViewChecked(): void {
        journal('ngAfterViewChecked');
      }
    
      ngOnDestroy(): void {
        clearInterval(this.minuteur);
        journal('ngOnDestroy : minuteur arrêté');
      }
    }
    <!-- carte-session.html -->
    <article class="carte">
      <h3 #titreEl>{{ titre() }}</h3>
      <p>Places restantes : <strong>{{ places() }}</strong></p>
      <p class="chrono">Carte affichée depuis {{ secondes() }} s</p>
      <ng-content />
    </article>
    /* carte-session.css */
    .carte {
      border: 1px solid #cbd5e1; border-radius: 10px;
      padding: 14px 16px; background: #fff; width: 320px;
    }
    h3 { color: #dd0031; margin: 0 0 8px; }
    p { margin: 4px 0; }
    .chrono { color: #64748b; font-size: 0.9rem; }
    • Nouveautés dans ce code : #titreEl et #note sont des variables de référence qui nomment une balise. viewChild('titreEl') retrouve une balise du template du composant ; contentChild('note') retrouve une balise du contenu projeté par le parent. Les deux renvoient un signal, lu avec ().
    • Lancer ng serve, ouvrir http://localhost:4200 puis la console du navigateur (F12, onglet Console).

    Application de démonstration du cycle de vie d'un composant Angular

    1. Étape 1 : la création

      • Au chargement, la console affiche les neuf messages suivants, toujours dans cet ordre :

    Console : ordre des hooks à la création d'un composant Angular

    • Plusieurs observations importantes :
      • Le constructeur passe en premier, avant que le parent ait transmis les inputs. Il ne faut donc pas y lire un input : le compilateur le refuse même pour un input obligatoire (voir les erreurs fréquentes).
      • ngOnChanges est appelée une fois par input reçu, avec l'ancienne valeur (undefined la première fois) et la nouvelle.
      • Dans ngOnInit, les inputs sont disponibles, mais la vue n'est pas encore affichée : la balise <h3> existe, mais son texte est vide.
      • Le contenu projeté est prêt avant la vue: ngAfterContentInit passe avant ngAfterViewInit.
      • Dans ngAfterViewInit, la vue est affichée : le <h3> contient le titre.
  • constructor ou ngOnInit ?

  •   constructor() ngOnInit()
    Appelé par JavaScript (new), avant toute intervention d'Angular sur le composant Angular, quand le composant est prêt
    Inputs disponibles non oui
    À y mettre inject() des services, déclaration de effect et de computed logique qui dépend des inputs, chargement de données, minuteurs
    • Règle simple : le constructeur prépare l'objet ; ngOnInit démarre le composant.
  • Étape 2 : la mise à jour

    • Cliquer sur « Réserver une place », puis sur « Changer le titre », enfin deux fois sur « Afficher / masquer » :

    Console : hooks Angular appelés lors de la mise à jour et de la destruction d'un composant

    • À chaque clic, Angular vérifie les composants (c'est la détection de changements) :
      • ngOnChanges n'est appelée que si un input a changé, et changes ne contient que les inputs modifiés ;
      • ngDoCheck, ngAfterContentChecked et ngAfterViewChecked sont appelées à chaque vérification ;
      • ngOnInit, ngAfterContentInit et ngAfterViewInit ne sont jamais rappelées.
    • Et le chronomètre ? Il modifie chaque seconde le signal secondes, lu uniquement dans le template de la carte. Angular met alors à jour ce seul affichage, sans rappeler les hooks : la console ne montre rien. C'est l'un des avantages des signaux : moins de travail à chaque changement.
  • Lire les changements avec SimpleChanges

    • L'objet changes associe à chaque input modifié un objet SimpleChange:
    Propriété Contenu
    previousValue l'ancienne valeur (undefined au premier appel)
    currentValue la nouvelle valeur
    firstChange true lors du premier appel
    isFirstChange() méthode qui renvoie la même information
    ngOnChanges(changes: SimpleChanges): void {
      const places = changes['places'];
      if (places && !places.firstChange && places.currentValue <= 3) {
        console.log('Attention : plus que', places.currentValue, 'places !');
      }
    }
    • Avec les inputs signaux (input()), on a souvent plus simple que ngOnChanges: une valeur calculée avec computed, recalculée automatiquement quand l'input change, ou un effect pour exécuter une action. Exemple : protected presqueComplet = computed(() => this.places() <= 3);
  • Étape 3 : la destruction

    • Masquer la carte la retire du DOM : Angular appelle ngOnDestroy, puis supprime l'objet. La réafficher crée une nouvelle instance, qui repart du constructeur (son chronomètre repart de 0, et elle reçoit les valeurs actuelles des inputs).
    • ngOnDestroy sert à nettoyer tout ce que le composant a démarré et qu'Angular ne peut pas arrêter seul :
      • les minuteurs : setInterval, setTimeout;
      • les écouteurs ajoutés à la main : window.addEventListener(…);
      • les abonnements RxJS (subscribe) qui ne se terminent pas d'eux-mêmes ;
      • les connexions (WebSocket…), les bibliothèques JavaScript initialisées dans ngAfterViewInit.
    • Que se passe-t-il si on oublie ? Retirons clearInterval(this.minuteur) de ngOnDestroy et ajoutons un message tic dans le minuteur :

    Fuite de mémoire Angular : un minuteur continue après ngOnDestroy

    • Le composant n'est plus affiché, mais son minuteur continue de tourner et garde l'objet en mémoire : c'est une fuite (memory leak). Chaque affichage en ajoute une : dans une vraie application, on accumule des traitements inutiles, des requêtes en double et des ralentissements.
  • ngAfterViewInit : travailler avec le DOM affiché

    • ngAfterViewInit est le premier moment où le template est réellement affiché. On l'utilise pour lire la taille d'un élément, donner le focus à un champ, ou initialiser une bibliothèque JavaScript qui a besoin d'une balise (un graphique, une carte géographique…).
    • Attention à modifier l'affichage à cet endroit. Si l'on change dans ngAfterViewInit une propriété simple affichée par le template (this.etat = 'prêt'), la vue vient d'être vérifiée : avec Angular 22, la page continue d'afficher l'ancienne valeur (« chargement ») jusqu'à la vérification suivante, sans aucun message. Avec un signal (this.etat.set('prêt')), Angular sait qu'il doit mettre la vue à jour et affiche bien « prêt ». Dans les projets plus anciens, la première écriture provoque l'erreur NG0100: ExpressionChangedAfterItHasBeenCheckedError.
  • Les fonctions modernes

    • Les versions récentes d'Angular proposent des fonctions, à appeler dans le constructeur, qui remplacent souvent les hooks :
    Fonction Rôle Remplace souvent
    computed(() => …) valeur calculée à partir de signaux (dont les inputs), recalculée automatiquement ngOnChanges pour calculer une valeur
    effect(() => …) action relancée chaque fois qu'un signal lu change ngOnChanges pour déclencher une action
    afterNextRender(() => …) une fois, après le prochain affichage dans le DOM ngAfterViewInit
    afterEveryRender(() => …) après chaque affichage ngAfterViewChecked
    inject(DestroyRef).onDestroy(() => …) nettoyage à la destruction ngOnDestroy
    // src/app/carte-moderne/carte-moderne.ts
    import {
      Component, DestroyRef, ElementRef, OnInit,
      afterNextRender, effect, inject, input, signal, viewChild,
    } from '@angular/core';
    
    @Component({
      selector: 'app-carte-moderne',
      styleUrl: './carte-moderne.css',
      templateUrl: './carte-moderne.html',
    })
    export class CarteModerne implements OnInit {
      titre = input.required<string>();
      places = input(0);
    
      protected secondes = signal(0);
      private titreEl = viewChild.required<ElementRef<HTMLHeadingElement>>('titreEl');
    
      constructor() {
        journal('constructor');
    
        // Relancé à chaque fois que places() change
        effect(() => journal(`effect : places = ${this.places()}`));
    
        // Une seule fois, quand le composant est affiché dans le DOM
        afterNextRender(() => {
          const hauteur = this.titreEl().nativeElement.offsetHeight;
          journal(`afterNextRender : hauteur du <h3> = ${hauteur} px`);
        });
    
        // Nettoyage déclaré au même endroit que la ressource
        const minuteur = setInterval(() => this.secondes.update((s) => s + 1), 1000);
        inject(DestroyRef).onDestroy(() => {
          clearInterval(minuteur);
          journal('DestroyRef.onDestroy : minuteur arrêté');
        });
      }
    
      ngOnInit(): void {
        journal(`ngOnInit : titre = « ${this.titre()} »`);
      }
    }

    Console : effect, afterNextRender et DestroyRef dans un composant Angular

    • L'avantage principal de DestroyRef: la ressource et son nettoyage sont écrits côte à côte, on ne peut pas oublier l'un sans voir l'autre. Pour un abonnement RxJS, l'opérateur takeUntilDestroyed() (de @angular/core/rxjs-interop) fait le même travail.
    • afterNextRender ne s'exécute que dans le navigateur (pas lors d'un rendu côté serveur) : c'est l'endroit sûr pour utiliser window, document ou une bibliothèque qui manipule le DOM.
  • Bonnes pratiques

    • Écrire implements OnInit (et les autres interfaces) : une faute de frappe comme ngOninit() est alors signalée par le compilateur au lieu d'être ignorée en silence.
    • Garder le constructeur léger ; démarrer le composant dans ngOnInit.
    • Tout ce qui est démarré doit être arrêté : un setInterval ou un subscribe appellent un nettoyage dans ngOnDestroy ou DestroyRef.
    • Ne pas mettre de traitement lourd dans ngDoCheck et les méthodes « Checked ».
    • Préférer computed et effect à ngOnChanges avec les inputs signaux.
  • Activité 1 : observer le cycle de vie

    • Reproduire le projet cycle-de-vie, ouvrir la console et noter l'ordre des messages au chargement.
    • Cliquer trois fois sur « Réserver une place » : combien de messages par clic ? Lesquels ?
    • Expliquer pourquoi le chronomètre de la carte avance sans provoquer de message dans la console.
  • Activité 2 : plusieurs instances

    • Dans app.html, afficher une deuxième carte avec le titre « Administration Linux » et 5 places. Dans quel ordre apparaissent les messages des deux cartes au chargement ? Les numéros des messages se suivent-ils ?
    • Ajouter au journal le titre de la carte, pour savoir quelle instance parle.
  • Activité 3 : réagir à un input

    • Dans ngOnChanges, afficher un avertissement dans la console quand il reste 3 places ou moins, sauf au premier appel (firstChange).
    • Obtenir le même résultat sans ngOnChanges: un computed presqueComplet qui affiche un badge « Bientôt complet » dans la carte, et un effect pour le message de la console.
  • Activité 4 : provoquer et corriger une fuite

    • Ajouter console.log('tic', this.secondes()) dans le minuteur, puis retirer le clearInterval de ngOnDestroy. Masquer et réafficher la carte trois fois : combien de minuteurs tournent ?
    • Corriger la fuite. Puis réécrire le nettoyage avec inject(DestroyRef).onDestroy(…) et supprimer ngOnDestroy.
    • Dans un nouveau composant, écouter le redimensionnement de la fenêtre (window.addEventListener('resize', …)) pour afficher sa largeur, et retirer l'écouteur à la destruction (removeEventListener).
  • Activité 5 : donner le focus à un champ

    • Créer un composant recherche contenant un champ <input #champ>. Avec viewChild, donner le focus au champ dès qu'il est affiché (nativeElement.focus()).
    • Essayer d'abord dans ngOnInit, puis dans ngAfterViewInit, puis avec afterNextRender: noter ce qui se passe dans chaque cas. Afficher le composant dans un bloc @if et tester en le masquant et le réaffichant.
  • Activité 6 : retour sur le projet FormaPlus

    • Dans votre projet forma-plus, ajouter au composant d'accueil un bandeau qui fait défiler un message toutes les 5 secondes (« Inscriptions ouvertes », « Nouvelle session Angular »…), démarré dans ngOnInit et arrêté correctement à la destruction.
    • Faire un commit : git commit -m "Cycle de vie : bandeau défilant", puis un push.
  • Erreurs fréquentes

  • Erreur Angular NG8118 : lecture d'un input obligatoire dans le constructeur

    Problème Cause Solution
    NG8118: `titre` is a required `input` and does not have a value in this context. Input lu dans le constructeur (ou dans l'initialisation d'une propriété) Le lire dans ngOnInit, un computed ou un effect
    La méthode n'est jamais appelée, sans message Faute de frappe (ngOninit, ngOnDestroyed) Ajouter implements OnInit: le compilateur signale TS2420
    Traitements qui continuent après la fermeture d'un écran, actions en double Minuteur, écouteur ou abonnement non arrêté Nettoyer dans ngOnDestroy ou avec DestroyRef
    viewChild() renvoie undefined, ou un élément encore vide Élément lu trop tôt (dans le constructeur ou ngOnInit), ou placé dans un bloc @if faux Le lire dans ngAfterViewInit ou afterNextRender
    L'affichage ne se met pas à jour (ou NG0100 dans les anciens projets) Propriété simple affichée modifiée dans ngAfterViewInit Utiliser un signal, ou faire le calcul plus tôt (ngOnInit, computed)
    Application lente Code lourd dans ngDoCheck ou une méthode « Checked » Déplacer le code, utiliser computed
    1. À retenir

      • Un composant est créé, mis à jour puis détruit; Angular appelle ses hooks à chaque étape.
      • Ordre à la création : constructor → ngOnChanges → ngOnInit → ngDoCheck → ngAfterContentInit → ngAfterContentChecked → ngAfterViewInit → ngAfterViewChecked.
      • Les inputs sont disponibles dans ngOnInit, pas dans le constructeur ; la vue est affichée dans ngAfterViewInit.
      • ngOnChanges reçoit uniquement les inputs modifiés (previousValue, currentValue, firstChange).
      • ngOnDestroy (ou DestroyRef) sert à arrêter tout ce que le composant a démarré.
      • Avec les signaux : computed, effect, afterNextRender et DestroyRef remplacent souvent les hooks.
    Un composant bien écrit sait quand commencer, quand réagir et surtout quand s'arrêter : tout ce qu'il démarre dans ngOnInit, il le range dans ngOnDestroy.