À 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.
-
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 :
constructoroungOnInit,ngOnInitoungAfterViewInit - Réagir au changement d'un input avec
ngOnChangesetSimpleChanges - Libérer les ressources dans
ngOnDestroypour éviter les fuites - Connaître les fonctions récentes qui complètent les hooks :
effect,afterNextRender,DestroyRef
-
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
@ifou 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éfixeng:ngOnInit()↔OnInit,ngOnDestroy()↔OnDestroy…
- 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

-
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,ngOnDestroyet, plus rarement,ngOnChangesetngAfterViewInit. Les méthodes « Checked » etngDoChecksont 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
CarteSessionqui implémente les huit hooks et écrit un message dans la console à chaque appel. Le composant parentApppermet 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
CarteSessionreçoit deux inputs (titreetplaces), affiche depuis combien de secondes il existe (un minuteur démarré dansngOnInitet arrêté dansngOnDestroy), 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 :
#titreElet#notesont 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, ouvrirhttp://localhost:4200puis la console du navigateur (F12, onglet Console).

-
Étape 1 : la création
- Au chargement, la console affiche les neuf messages suivants, toujours dans cet ordre :

- 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).
ngOnChangesest appelée une fois par input reçu, avec l'ancienne valeur (undefinedla 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:
ngAfterContentInitpasse avantngAfterViewInit. - 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 ;
ngOnInitdé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 » :

- À chaque clic, Angular vérifie les composants (c'est la détection de changements) :
ngOnChangesn'est appelée que si un input a changé, etchangesne contient que les inputs modifiés ;ngDoCheck,ngAfterContentCheckedetngAfterViewCheckedsont appelées à chaque vérification ;ngOnInit,ngAfterContentInitetngAfterViewInitne 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
changesassocie à chaque input modifié un objetSimpleChange:
| 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 quengOnChanges: une valeur calculée aveccomputed, recalculée automatiquement quand l'input change, ou uneffectpour 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). ngOnDestroysert à 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.
- les minuteurs :
- Que se passe-t-il si on oublie ? Retirons
clearInterval(this.minuteur)dengOnDestroyet ajoutons un messageticdans le minuteur :

- 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é
ngAfterViewInitest 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
ngAfterViewInitune 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'erreurNG0100: 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()} »`);
}
}

- 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érateurtakeUntilDestroyed()(de@angular/core/rxjs-interop) fait le même travail. afterNextRenderne s'exécute que dans le navigateur (pas lors d'un rendu côté serveur) : c'est l'endroit sûr pour utiliserwindow,documentou une bibliothèque qui manipule le DOM.
Bonnes pratiques
- Écrire
implements OnInit(et les autres interfaces) : une faute de frappe commengOninit()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
setIntervalou unsubscribeappellent un nettoyage dansngOnDestroyouDestroyRef. - Ne pas mettre de traitement lourd dans
ngDoChecket les méthodes « Checked ». - Préférer
computedeteffectàngOnChangesavec 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
journalle 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: uncomputedpresqueCompletqui affiche un badge « Bientôt complet » dans la carte, et uneffectpour le message de la console.
Activité 4 : provoquer et corriger une fuite
- Ajouter
console.log('tic', this.secondes())dans le minuteur, puis retirer leclearIntervaldengOnDestroy. 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 supprimerngOnDestroy. - 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
recherchecontenant un champ<input #champ>. AvecviewChild, donner le focus au champ dès qu'il est affiché (nativeElement.focus()). - Essayer d'abord dans
ngOnInit, puis dansngAfterViewInit, puis avecafterNextRender: noter ce qui se passe dans chaque cas. Afficher le composant dans un bloc@ifet 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é dansngOnInitet arrêté correctement à la destruction. - Faire un commit :
git commit -m "Cycle de vie : bandeau défilant", puis un push.
Erreurs fréquentes

| 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 |
-
À 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 dansngAfterViewInit. ngOnChangesreçoit uniquement les inputs modifiés (previousValue,currentValue,firstChange).ngOnDestroy(ouDestroyRef) sert à arrêter tout ce que le composant a démarré.- Avec les signaux :
computed,effect,afterNextRenderetDestroyRefremplacent 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 dansngOnInit, il le range dansngOnDestroy.