When does Blazor (not) render?

6/23/2022
8 minute read

Blazor is an awesome UI framework which brings the power and ease of .NET to your browser. At certain points Blazor does automatically render your UI, but there are also some "pitfalls" you have to be aware about. As a small disclaimer I assume that your components derive from ComponentBase which is the default when you create "normal" Blazor components. From a technical point of view IComponent would satisfy the Blazor renderer, but if you use this, I think you already know what will come next.

Let's begin with the obvious candiate.

StateHasChanged

StateHasChanged is a protected method, which basically tells the Blazor renderer: "Hey please redraw my compoennt". This can be for example very handy in conjunction with timers or incomplete asynchronous tasks. The latter one will be addressed later in that article.

Please be aware that StateHasChanged has to be called from the UI thread otherwise you will get an exception. On Blazor WASM there is currently only one thread available, but still it is good to use InvokeAsync to guarantee that StateHasChanged is executed on the right context.

Timer = new System.Threading.Timer(_ =>
{
  InvokeAsync(StateHasChanged);
}, null, 500, 500);

OnAfterRender

This method is called after the Blazor renderer has done all the other lifecycle events. As the name suggests this lifeycle method is called after the last render. That means if you update some parameter, which should be reflected in the UI, though luck with this one. Blazor will not render anything here. If you want to render something in OnAfterRender you can utilize our friend StateHasChanged.

async stuff

A lot of the lifecycle events are async (OnInitializedAsync), as well as event handlers (MyEvent.InvokeAsync(someProp)). Asynchronous stuff is a bit special. Everytime on the first await boundary the Blazor pipeline will trigger a re-render and after the Task is complete. On the Microsoft page, which I will link in the resources below, you can see this for OnInitizaliedAsync and OnParamtersSetAsync:

Lifecylce

As well as for event handlers:

event handlers

Now what does that mean in code? Just imagine the OnInitializedAsync method:

protected async Task OnInitializedAsync()
{
    var userName = SomeSyncOperation();

    // This is the first await
    // after the Task started and we give back the control to the caller Blazor will render
    var employees = await employeeService.GetByUsernameAsync(userName);
    var tranformed = transformer.Transform(employees);
    // We are done and Blazor renders again
}

All the time I explicitly stated after the first await bounday and after the task is done. So if you have the following code, this will still only 2 render cycles:

<ProgressBar Progess="@progress"></ProgressBar>
@code { 
    private int progress = 0;

    protected async Task OnInitializedAsync()
    {

        progress = 10;

        // Here the UI will update the progress from 0 to 10
        var user = await GetUserByIdAsync(Id);

        // This will not be reflected on the UI 
        progress = 25;
        var games = await GetHighScoreFromUserAsync(user);

        // Still not 
        progress = 75;
        someObject = await DoAnotherasync(games);

        progress = 100;
        // After we are dine here the UI will jump from 10 to 100
    } 
}

The question is now: Why does it change only after the first time and after we are completely done? The first time is pretty easy to answer. Before we hit any await boundary the UI-Thread is occupied by our OnInitializedAsync. Only when we hit the await we (the OnInitializedAsync method) give back control to the caller. So that makes sense to a certain degree, but why not after the second or third await?. And the reason is that Blazor has no means to detect how "far" your method has run until now. Any Task has a TaskStatus property which tells the current state of the task. And this state will not change after the first await boundary (except of course you have an exception). It only changes when your task ran into completion. So summarized: Everytime your TaskStatus changes... which does not happen on every await encounter.

So how to fix that? And the answer is again: StateHasChanged. In terms off the example given above:

<ProgressBar Progess="@progress"></ProgressBar>
@code { 
    private int progress = 0;

    protected async Task OnInitializedAsync()
    {

        progress = 10;

        // Here the UI will update the progress from 0 to 10
        var user = await GetUserByIdAsync(Id);

        progress = 25;
        // Force a re-render
        StateHasChanged();

        var games = await GetHighScoreFromUserAsync(user);

        progress = 75;
        // Force a re-render
        StateHasChanged();
        someObject = await DoAnotherasync(games);

        progress = 100;
        // After we are dine here the UI will jump from 10 to 100
    } 
}

I created an example on BlazorFiddle where you can see this live in action.

ShouldRender

Last but not least there is also a property called ShouldRender and it does exactly what it says. If you set it to false no re-render will be triggered. That might be very helpful if you have a batch of events and you don't want to re-render every single item.

A small example: Just imagine you get a list of items one by one. You know exactly how many items you expect, so there is no need to render everytime you get one item. You will only render if you received all the items.

Parameters

If you have a typical parent child combination like this:

<ChildComponent Value="@myValue"></ChildComponent>

Updating myValue will lead to a new render cycle for the parent and the child. If the child component internally changes the passed myValue the parent will not update. The reason is that those binding are one-way bindings. That said if somehow your child component influences your parent component most probably you need to call StateHasChanged in your child component. An example would be modal dialogs, depending the way they are implemented.

If you use two-way binding, this looks of course different.

<ChildTwoWayBind @bind-Value="myValue"></ChildTwoWayBind>

Of course they same as for "normal" aka one-way binding applies: The parent and the child get re-rendered. The special case is that even if the child changes the value, the parent re-renders as well. Internally there are event handlers working, basically applying the logic we discussed earlier. More about the bind-directive can be found on the Microsoft site.

Another special form is the CascadingParameter. Everytime the cascading value changes all child components using that parameter will also be re-rendered. That is why it makes it also quite expensive. First all the events have to be activated and then all the children have to re-render. If you only need the initial value of a cascading value and never interested in updates of such you can provide the IsFixed value. That will cause no re-renders and lighter components as the event chaining can be omitted:

<CascadingValue Value="@parentCascadeParameter1" Name="CascadeParam1" IsFixed="true">
...
</CascadingValue>

Conclusion

Blazor has a lot of predefined lifecycle events where render cycles are automatically invoked. But also there are some scenario which are not automatically covered.

Resources

Blazor .NET 8 - Enhanced Form Navigation

There are many new cool features with .NET 8 and Blazor in particular. In this blog post, I want to highlight a feature that I believe is very useful in the new context Blazor is living.

.NET 8 and Blazor United / Server-side rendering

New .NET and new Blazor features. In this blog post, I want to highlight the new features that are hitting us with .NET 8 in the Blazor world. So let's see what's new.

Blazor Project Structure

Did you ever wonder what is a nice way of structuring your Blazor application?

I will show you how I structure my Blazor projects (as well as this very blog). What are the upside in contrast to the "default" structuring you get with the Blazor template.

An error has occurred. This application may no longer respond until reloaded. Reload x