Blazor : le cycle de vie
Les méthodes de cycle de vie d'un composant : OnInitialized, OnParametersSet, OnAfterRender (firstRender), ShouldRender et StateHasChanged, Dispose, plus le piège du rendu asynchrone et du prerendering.
Blazor : le cycle de vie
De sa création à sa destruction, un composant Blazor traverse une séquence de méthodes que tu peux redéfinir pour brancher ta logique au bon moment : charger des données, faire du JS interop, nettoyer. Comprendre l'ordre, c'est éviter les bugs de rendu et les fuites mémoire.
# La séquence
Quand un composant apparaît puis vit, Blazor appelle ses méthodes dans cet ordre :
| Étape | Méthode | Quand | Combien de fois |
|---|---|---|---|
| 1 | OnInitialized[Async] | à la création, après les premiers paramètres | une fois |
| 2 | OnParametersSet[Async] | après (re)réception des paramètres | à chaque changement |
| 3 | (rendu) | Blazor produit le DOM | à chaque rendu |
| 4 | OnAfterRender[Async] | après la mise à jour du DOM | à chaque rendu |
| 5 | Dispose / DisposeAsync | à la destruction | une fois |
ShouldRender() s'intercale avant chaque rendu (sauf le premier) pour l'autoriser ou non ; StateHasChanged() déclenche un rendu à la demande.
# OnInitialized : le point de départ
Appelée une seule fois, c'est l'endroit pour l'initialisation et le chargement des données.
@if (produits is null)
{
<p>Chargement...</p>
}
else
{
<ul>@foreach (var p in produits) { <li>@p.Nom</li> }</ul>
}
@code {
private List<Produit>? produits;
protected override async Task OnInitializedAsync()
{
produits = await Service.GetProduitsAsync();
}
}@if (produits is null){ <p>Chargement...</p>}else{ <ul>@foreach (var p in produits) { <li>@p.Nom</li> }</ul>} @code { private List<Produit>? produits; protected override async Task OnInitializedAsync() { produits = await Service.GetProduitsAsync(); }} Rendu avant la fin de l'await : Blazor rend le composant une première fois dès qu'il tombe sur le premier await, donc pendant que les données chargent. C'est pour ça qu'il faut gérer l'état de chargement (produits is null ici) : sans ça, tu tentes d'itérer sur null. Une fois la tâche finie, Blazor re-rend automatiquement.
# OnParametersSet : à chaque changement de paramètre
Appelée après OnInitialized, puis chaque fois que le parent fournit de nouveaux paramètres. C'est ici qu'on réagit à un [Parameter] qui change.
@code {
[Parameter] public int ClientId { get; set; }
private Commande[]? commandes;
protected override async Task OnParametersSetAsync()
{
// Recharge les commandes quand le ClientId change
commandes = await Service.GetCommandesAsync(ClientId);
}
}@code { [Parameter] public int ClientId { get; set; } private Commande[]? commandes; protected override async Task OnParametersSetAsync() { // Recharge les commandes quand le ClientId change commandes = await Service.GetCommandesAsync(ClientId); }}OnInitialized sert à ce qui ne dépend pas des paramètres (une seule fois) ; OnParametersSet à ce qui en dépend (à chaque changement).
# OnAfterRender : après le rendu du DOM
Appelée une fois le DOM à jour. Le paramètre firstRender distingue le premier rendu des suivants. C'est le seul endroit sûr pour le JS interop (le DOM existe) : initialiser une lib tierce, un graphique, donner le focus.
@code {
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("initGraphique", "#chart");
}
}
}@code { protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await JS.InvokeVoidAsync("initGraphique", "#chart"); } }} Ne déclenche jamais un rendu sans condition dans OnAfterRender. Appeler StateHasChanged() sans garde provoque : rendu → OnAfterRender → StateHasChanged → rendu → ... boucle infinie. Protège toujours par firstRender ou une condition.
# ShouldRender & StateHasChanged : maîtriser le rendu
Blazor re-rend automatiquement après chaque événement UI et chaque méthode de cycle de vie. Deux leviers pour reprendre la main :
StateHasChanged(): forcer un rendu, indispensable quand l'état change hors du flux Blazor (un timer, la complétion d'une tâche async, un événement d'un service).ShouldRender(): bloquer un rendu coûteux quand rien n'a visuellement changé.
@code {
private int _secondes;
private Timer? _timer;
protected override void OnInitialized()
{
// Le callback du timer tourne hors du contexte de Blazor :
// InvokeAsync remet le travail sur le bon thread, StateHasChanged rafraîchit.
_timer = new Timer(_ => InvokeAsync(() =>
{
_secondes++;
StateHasChanged();
}), null, 0, 1000);
}
}@code { private int _secondes; private Timer? _timer; protected override void OnInitialized() { // Le callback du timer tourne hors du contexte de Blazor : // InvokeAsync remet le travail sur le bon thread, StateHasChanged rafraîchit. _timer = new Timer(_ => InvokeAsync(() => { _secondes++; StateHasChanged(); }), null, 0, 1000); }}# Dispose : le nettoyage
Un composant qui s'abonne à quelque chose (timer, événement d'un service, IJSObjectReference) doit se désabonner quand il disparaît, sinon fuite mémoire. On implémente IDisposable (ou IAsyncDisposable).
@implements IDisposable
@code {
private Timer? _timer;
// ... initialisation du timer ...
public void Dispose() => _timer?.Dispose();
}@implements IDisposable @code { private Timer? _timer; // ... initialisation du timer ... public void Dispose() => _timer?.Dispose();}C'est exactement ce qu'exige le pattern state container vu dans l'injection de dépendances : OnInitialized s'abonne à OnChange, Dispose s'y désabonne.
# Le piège du prerendering
Avec le prerendering (activé par défaut pour les modes interactifs en .NET 8, voir l'hébergement), OnInitializedAsync s'exécute deux fois : une fois sur le serveur pour produire le HTML initial, une fois quand le composant devient interactif. Résultat : ta requête de données part deux fois.
Pour ne charger qu'une fois, on persiste l'état du prerender avec PersistentComponentState, ou on désactive le prerendering sur le composant si ce n'est pas gênant.
# À retenir
- Ordre :
OnInitialized(une fois) →OnParametersSet(à chaque changement de paramètre) → rendu →OnAfterRender→Dispose. OnInitializedAsyncrend avant la fin de l'await: gère toujours l'état de chargement.- JS interop uniquement dans
OnAfterRender, gardé parfirstRender; jamais deStateHasChangednon gardé (boucle infinie). StateHasChangedforce un rendu (timer, async, événement externe) ;ShouldRenderen évite un inutile.Disposepour se désabonner (timers, événements, JS) et éviter les fuites.- Piège prerendering :
OnInitializedAsyncs'exécute deux fois ;PersistentComponentStatepour ne charger qu'une fois.