Version information
.NET Core SDK: 3.1.302
ASP.NET Core Blazor WebAssembly: 3.2.1
Demo Application
To get started right away, I use the demo application that comes with the SDK. To do so, we create a new Blazor WebAssembly project and make sure it works properly.
dotnet new blazorwasm -o BlazorApp1
cd BlazorApp1
dotnet run
First, we implement a new component to visualize the lifecycle. Create a new file DemoComponent.razor
in the Shared
folder, which overrides (almost) all methods. The only method we ignore is ShouldRender
, which is useful for some advanced use cases (like performance optimization). We may talk about ShouldRender
in the future.
@using Microsoft.Extensions.Logging
@implements IDisposable
@inject ILogger Logger
Demo Component
@code {
public DemoComponent()
{
// "Logger" is not initialized yet
}
public override async Task SetParametersAsync(ParameterView parameters)
{
Logger.LogInformation("SetParametersAsync-start");
await base.SetParametersAsync(parameters);
Logger.LogInformation("SetParametersAsync-end");
}
protected override void OnInitialized()
{
Logger.LogInformation("OnInitialized-start");
base.OnInitialized();
Logger.LogInformation("OnInitialized-end");
}
protected override async Task OnInitializedAsync()
{
Logger.LogInformation("OnInitializedAsync-start");
await base.OnInitializedAsync();
Logger.LogInformation("OnInitializedAsync-end");
}
protected override void OnParametersSet()
{
Logger.LogInformation("OnParametersSet-start");
base.OnParametersSet();
Logger.LogInformation("OnParametersSet-end");
}
protected override async Task OnParametersSetAsync()
{
Logger.LogInformation("OnParametersSetAsync-start");
await base.OnParametersSetAsync();
Logger.LogInformation("OnParametersSetAsync-end");
}
protected override void OnAfterRender(bool firstRender)
{
Logger.LogInformation("OnAfterRender({firstRender})-start", firstRender);
base.OnAfterRender(firstRender);
Logger.LogInformation("OnAfterRender({firstRender})-end", firstRender);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
Logger.LogInformation("OnAfterRenderAsync({firstRender})-start", firstRender);
await base.OnAfterRenderAsync(firstRender);
Logger.LogInformation("OnAfterRenderAsync({firstRender})-end", firstRender);
}
public void Dispose()
{
Logger.LogInformation("Dispose");
}
}
Now, we put the component onto the page Index.razor
.
@page "/"
The component renders text only.
The logger output of the component should look something like the following:
SetParametersAsync-start
OnInitialized-start
OnInitialized-end
OnInitializedAsync-start
OnInitializedAsync-end
OnParametersSet-start
OnParametersSet-end
OnParametersSetAsync-start
OnParametersSetAsync-end
SetParametersAsync-end
OnAfterRender(True)-start
OnAfterRender(True)-end
OnAfterRenderAsync(True)-start
OnAfterRenderAsync(True)-end
Blazor Component Lifecycle
The lifecycle of a Blazor component begins when it is rendered on the page, meaning that it becomes visible for the first time. This might happen after navigating to a page with the corresponding component or by the evaluation of statements like if
to true
.
For example, if the variable _renderDemoComponent
is false
after navigating to a page then the DemoComponent
will not be created until the if
statement evaluates to true
. Then again, if _renderDemoComponent
is true
and becomes false
then the DemoComponent
will not just be hidden but destroyed. The same happens when navigating to another page, which leads to the disposal of all components of the current page.
@if (_renderDemoComponent)
{
}
The canonical lifecycle of a Blazor component is pretty linear, at least on initial rendering. It looks like this:
Constructor
SetParametersAsync
OnInitialized
/OnInitializedAsync
OnParametersSet
/OnParametersSetAsync
OnAfterRender
/OnAfterRenderAsync
IDisposable.Dispose()
(if we navigate to another page)
Note: The interface IAsyncDisposable
is not supported.
The Easy Parts about the Lifecycle
Some methods are called only once, which makes it easier to understand the lifecycle. The first one is the Constructor
, which is more or less useless in Blazor components because the constructor dependency injection is not supported. The other is a method pair OnInitialized
/OnInitializedAsync
, which is called after the initial(!) setting of the parameters. This method pair is kind of the constructor of a Blazor component and is useful for the initialization of the component. The last one is the method Dispose
, which is called if the component is implementing the interface IDisposable
. This method should be used for all kinds of cleanup, like unsubscribing from events and for stopping asynchronous calls.
The Misleading Parts about the Lifecycle
Although the method Dispose
is called last, there is no warranty that all previously started asynchronous calls (like in OnAfterRenderAsync
) are finished. All asynchronous calls should have CancellationToken
support and be canceled in Dispose
.
One of the programming behaviors I see very often is overriding a virtual method without calling the base method. Most of the time, this poses no impact on the component because the base methods are empty, but the method SetParametersAsync
is different.
If the base method SetParametersAsync
is not called …
public override async Task SetParametersAsync(ParameterView parameters)
{
Logger.LogInformation("SetParametersAsync-start");
//await base.SetParametersAsync(parameters);
Logger.LogInformation("SetParametersAsync-end");
}
…then the lifecycle is broken because subsequent methods like OnInitialized
are not called and the component is not rendered.
The Complex Parts about the Lifecycle
The remaining three methods, or method pairs, may be called multiple times during the lifetime of a component.
In general, if there is an asynchronous call, the lifecycle gets messy because the calls may overlap. To see this in action, we add an asynchronous method to OnInitializedAsync.
protected override async Task OnInitializedAsync()
{
Logger.LogInformation("OnInitializedAsync-start");
await base.OnInitializedAsync();
await Task.Yield();
Logger.LogInformation("OnInitializedAsync-end");
}
According to the logger output below, the lifecycle changed quite a lot. The (initial) rendering comes before OnParametersSet
and is triggered twice in total. Before seeing this, the method OnParametersSet
(due to its name) may look more suitable for initial(!) processing of the parameters, but that’s not always the case. As we can see, the method OnParametersSet
should not be preferred to OnInitialized
/OnInitializedAsync
because otherwise the component will be rendered without initialization.
SetParametersAsync-start
OnInitialized-start
OnInitialized-end
OnInitializedAsync-start
OnAfterRender(True)-start
OnAfterRender(True)-end
OnAfterRenderAsync(True)-start
OnAfterRenderAsync(True)-end
OnInitializedAsync-end
OnParametersSet-start
OnParametersSet-end
OnParametersSetAsync-start
OnParametersSetAsync-end
OnAfterRender(False)-start
OnAfterRender(False)-end
OnAfterRenderAsync(False)-start
OnAfterRenderAsync(False)-end
SetParametersAsync-end
Another pitfall is to assume that the method SetParametersAsync
is called every time the parameters are changed, but that is not always true. Under some conditions, the method is called even if the parameters are still the same.
For more information see: Don’t create components that write to their own parameter properties.
Last but not least, there is the method-pair OnAfterRender
/OnAfterRenderAsync
, which is called more often than the rest of the lifecycle methods. In previous chapters, we saw that asynchronous calls may change the order in which the methods are called, but that is not all. What I did not point out before is that the methods may overload with its previous calls. In other words, the method pair OnAfterRender
/OnAfterRenderAsync
may be called (again) although the previous execution of OnAfterRenderAsync
is not finished yet.
To demonstrate the overlapping, we add a <script>
at the end of the index.html
.
...