Blazorのライフサイクル
BlazorでWebアプリを開発していると、「APIからデータを取得するのはどこだっけ?」「JavaScript(JSInterop)を呼ぶのはどのメソッドだっけ?」「なぜか初期化処理が2回走るんだけど…」と毎回調べる羽目になります。
私はこういうライフサイクルの細かい実行順序を全然覚えられず、少し間が空くとすぐに忘れてしまうので、未来の自分が楽をするための備忘録としてまとめておきます。
この記事では、Blazor(Razorコンポーネント)のライフサイクルメソッドの実行順序、各メソッドの役割、そしてよく遭遇するハマりどころについて紹介します。
Blazorライフサイクルメソッド一覧
はじめに、Blazorでよく使う主なライフサイクルメソッドを一覧表にまとめました。
| 同期メソッド | 非同期メソッド | 実行タイミング | 主な用途 |
|---|---|---|---|
| - | SetParametersAsync | 親からパラメータが渡された時(一番最初) | パラメータの直接処理、カスタム初期化 |
OnInitialized | OnInitializedAsync | コンポーネントの初期化時(1回のみ) | 初期データのロード、サービス初期化 |
OnParametersSet | OnParametersSetAsync | パラメータ設定・変更時(初回および更新時) | パラメータに応じたプロパティ再計算・再取得 |
ShouldRender | - | コンポーネント再レンダリング直前 | レンダリング抑制によるパフォーマンス向上 |
OnAfterRender | OnAfterRenderAsync | コンポーネントのDOMレンダリング完了後 | JSInteropの呼び出し、DOM要素の操作 |
Dispose | DisposeAsync | コンポーネント破棄(アンマウント)時 | イベント購読解除、タイマー停止、リソース解放 |
ライフサイクルの実行フロー
コンポーネントが生成されてから破棄されるまでの大まかな流れは以下の通りです。
【コンポーネント生成・初回レンダリング時】
1. SetParametersAsync
↓
2. OnInitialized / OnInitializedAsync
↓
3. OnParametersSet / OnParametersSetAsync
↓
4. 初回レンダリング(HTML生成・DOM反映)
↓
5. OnAfterRender / OnAfterRenderAsync (firstRender = true)
【パラメータ変更・再レンダリング時】
1. SetParametersAsync
↓
2. OnParametersSet / OnParametersSetAsync
↓
3. ShouldRender(trueなら描画へ、falseならスキップ)
↓
4. 再レンダリング(HTML再生成・DOM反映)
↓
5. OnAfterRender / OnAfterRenderAsync (firstRender = false)
【コンポーネント破棄時】
1. Dispose / DisposeAsync
初回の初期化時のみ OnInitialized が呼ばれ、以降親コンポーネントの更新等でパラメータが渡された際は OnParametersSet から再実行されるのがポイントです。
各ライフサイクルメソッドの詳細
それぞれのメソッドの役割と書き方を見ていきます。
1. SetParametersAsync
親コンポーネントからパラメータが渡された際、一番最初に実行されるメソッドです。
引数として渡される ParameterView からパラメータを取得し、コンポーネントの [Parameter] 属性が付いた各プロパティに値をセットします。
@code {
[Parameter]
public string? Message { get; set; }
public override async Task SetParametersAsync(ParameterView parameters)
{
// 渡されたパラメータの確認やカスタム処理
if (parameters.TryGetValue<string>(nameof(Message), out var value))
{
// Messageが渡された場合の事前処理など
}
// 通常はbase.SetParametersAsyncを必ず呼ぶ(プロパティへの代入と以降のライフサイクル実行のため)
await base.SetParametersAsync(parameters);
}
}
普段の開発ではオーバーライドする機会は少ないですが、パラメータがプロパティに設定される前の値を直接参照したい場合や、パフォーマンス最適化のために手動でパラメータを設定したい場合などに使用します。
オーバーライドした際は、必ず最後に await base.SetParametersAsync(parameters); を呼び出す必要があります。
2. OnInitialized / OnInitializedAsync
コンポーネントが生成された際に1度だけ実行されるメソッドです。
バックエンドAPIからのデータ取得や、コンポーネント内で使う初期値の設定など、最もよく使うメソッドです。
@code {
private List<WeatherForecast>? forecasts;
protected override async Task OnInitializedAsync()
{
// バックエンドAPIから非同期でデータを取得
forecasts = await Http.GetFromJsonAsync<List<WeatherForecast>>("api/weather");
}
}
【注意】
非同期版のOnInitializedAsyncを使用する場合、awaitの手前で1回目の中間レンダリングが走り、await完了後に2回目のレンダリングが走ります。
そのため、画面側でforecastsがnullの状態(ローディング表示など)を考慮したテンプレートを書く必要があります。
3. OnParametersSet / OnParametersSetAsync
親コンポーネントから [Parameter] 経由で値が渡された後や、URLのルートパラメータが変更された時に実行されます。
初回レンダリング時だけでなく、親から受け取るパラメータが変化するたびに呼ばれます。
@code {
[Parameter]
public int ItemId { get; set; }
private ItemDetail? item;
protected override async Task OnParametersSetAsync()
{
// パラメータのItemIdに応じたデータを再取得
item = await ItemService.GetItemByIdAsync(ItemId);
}
}
「ページを遷移せずにURLパラメータだけが変わった場合(例: /users/1 から /users/2 への遷移)」は OnInitialized は呼ばれず OnParametersSet だけが呼ばれるため、パラメータ依存のデータ更新はここに書く必要があります。
4. ShouldRender
UIの再レンダリングを行うかどうかを bool 値で制御するメソッドです。
@code {
[Parameter]
public int Counter { get; set; }
private int previousCounter;
protected override bool ShouldRender()
{
// 偶数の時だけ再描画する例
return Counter % 2 == 0;
}
}
デフォルトは true を返します。
頻繁に状態が変わるけれど画面描画を毎回しなくても良いケースなどで、パフォーマンスチューニングとして false を返すようにオーバーライドすることがあります。(普段の開発ではあまり触る機会はありません)
5. OnAfterRender / OnAfterRenderAsync
HTMLが生成され、ブラウザのDOMに反映された直後に実行されます。
引数に bool firstRender を受け取り、初回描画時(true)か2回目以降の再描画時(false)かを判定できます。
@inject IJSRuntime JS
@code {
private ElementReference inputElement;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// 初回レンダリング時のみ実行:JSライブラリの初期化やフォーカス設定
await JS.InvokeVoidAsync("initChartPlugin");
await inputElement.FocusAsync();
}
}
}
JavaScriptの関数を呼び出す(JSInterop)場合や、ElementReference を使ってDOM要素を操作する場合は、必ずこのメソッド内で行います。
6. Dispose / DisposeAsync
コンポーネントが画面から消える(アンマウントされる)時に実行されます。
利用する場合は、ファイルの先頭で @implements IDisposable または @implements IAsyncDisposable を宣言します。
@implements IDisposable
@code {
[Inject]
private NavigationManager Navigation { get; set; } = default!;
protected override void OnInitialized()
{
// イベントの購読
Navigation.LocationChanged += HandleLocationChanged;
}
private void HandleLocationChanged(object? sender, LocationChangedEventArgs e)
{
// URL変更時の処理
}
public void Dispose()
{
// コンポーネント破棄時に購読解除(メモリリーク防止)
Navigation.LocationChanged -= HandleLocationChanged;
}
}
イベントの購読解除、Timer の破棄、CancellationTokenSource のキャンセルなど、メモリリークを防ぐための後処理を記述します。
よくあるハマりどころ
Blazorのライフサイクル周りで初心者がハマりやすいポイントをまとめます。
1. OnInitializedAsyncでJSInteropを呼ぶとエラーになる
OnInitialized や OnInitializedAsync の時点では、まだブラウザのDOM要素が構築されていません。
そのため、このタイミングで IJSRuntime.InvokeVoidAsync などを呼ぶと InvalidOperationException(またはJS側での要素未検出エラー)が発生します。
JavaScriptを呼び出す処理は、必ず OnAfterRenderAsync(bool firstRender) の中で呼ぶようにします。
2. プリレンダリングでOnInitializedAsyncが2回実行される
Blazorの対話型レンダリング(InteractiveServerやInteractiveWebAssembly)でプリレンダリングが有効になっている場合、以下の順序で2回処理が走ります。
- サーバーサイド(プリレンダリング時): 初期HTML生成のために1回目の
OnInitializedAsyncが実行 - クライアントサイド(接続確立後): 対話モード開始時に2回目の
OnInitializedAsyncが実行
これにより、「APIリクエストが2回飛ぶ」「DBへのINSERT処理が2回走る」といった問題が発生します。
対策
- 初回レンダリング後だけ実行したい処理:
OnAfterRenderAsync内でif (firstRender)で実行する - データの2度取りを防ぐ: .NET 8以降であれば
PersistentComponentStateを使用してプリレンダリング時の状態をクライアントに引き継ぐ
終わりに
Blazorのライフサイクルについてまとめました。
普段実装していると「とりあえず OnInitializedAsync に書いておけば動く」となりがちですが、JSInteropのタイミングやURLパラメータ変更時の挙動、メモリリーク対策の Dispose など、ライフサイクルを正しく理解していないとバグの原因になりますね。
設定やメソッド名を忘れてしまった時は、この記事を見返してサクッと実装できるようにしたいと思います。
不明な点や誤っている点がありましたらコメントやSNSまでお願いします。