When does Blazor decide to render your UI?

October 3, 2020 · 4 minute read · Tags: blazor | javascript

I recently ran into an interesting question about the way Blazor renders and realised I wasn’t exactly clear on the some of the details myself.

This small post is an attempt to understand, and explain one specific nuance of how Blazor re-renders the UI.

A question for you

What would you expect to happen if you ran this code using Blazor?

@page "/list"

<button @onclick="GenerateMessages">Generate Messages</button>

<ul>
    @foreach (var message in _messages)
    {
        <li>@message</li>
    }
</ul>

@code {
    private List<string> _messages = new List<string>();
    
    public async Task GenerateMessages()
    {
        _messages.Add("Hello");
        _messages.Add("World");
        _messages.Add("How are you doing today?");
    }    
}

I’m guessing you’d expect one of two outcomes when you click the button,:

  1. Each message to appear one after the other
  2. All of the messages to appear at once

Turns out it’s number 2.

This is down to the way Blazor chooses when to re-render your component; in this case rendering once when the method has completed.

One simple way to observe what’s happening here is to override the OnAfterRender method, and log a message on every render. Whilst here we can add some logging to the GenerateMessages method as well…

    protected override void OnAfterRender(bool firstRender)
    {
        Console.WriteLine("Rendering: First Render == " + firstRender);
    }

    public async Task GenerateMessages()
    {
        Console.WriteLine("Start of GenerateMessages");
        
        _messages.Add("Hello");
        
        // await Task.Delay(1000);
        _messages.Add("World");

        // await Task.Delay(1000);
        _messages.Add("How are you doing today?");
        
        Console.WriteLine("End of GenerateMessages");
    }

With this in place when we run our little example, we see this in the browser console.

So there’s a initial render when we visit “/list” for the first time.

The second render occurs when we click the button, after GenerateMessages has done its work.

Taking control of the render

But what if we actually wanted to see the items being added to the list, one at a time?

Well first we’d need a way to slow things down and one way to achieve this would be with Task.Delay.

public async Task GenerateMessages()
{
    Console.WriteLine("Start of GenerateMessages");
    
    _messages.Add("Hello");
    
    await Task.Delay(1000);
    _messages.Add("World");

    await Task.Delay(1000);
    _messages.Add("How are you doing today?");
    
    Console.WriteLine("End of GenerateMessages");
}

Run this and a potentially counter-intuitive thing happens…

“Hello” is shown as soon as we click the button, then there’s a two second pause before “World” and “How are you doing today?” are shown at exactly the same time!

So what gives?

When we call GenerateMessages, we’re calling an async method which means it can run in the background.

The method suspends the current workflow when we hit that first await Task.Delay(1000) and waits for that task to complete, but without blocking the thread.

This means the original thread (the UI thread that Blazor was using when we clicked our button) is freed up, at which point Blazor happily re-renders and our first message appears.

Before this the CLR captures the synchronization context (so it knows where to continue execution once this task is completed). In this case that synchronization context will put us back on Blazor’s UI thread when the long-running task concludes.

Our long running Task.Delay task continues, then concludes, at which point “World” is added to the list.

await Task.Delay(1000); // this concludes
_messages.Add("World"); // this is executed

However, as before (when all the items appeared at once), nothing implicitly instructs Blazor to re-render the UI at this point.

So our method continues, with no render taking place, meaning we get to the final message, and then the end of our method.

await Task.Delay(1000);
_messages.Add("How are you doing today?");

Console.WriteLine("End of GenerateMessages");

At this point, just as before, Blazor will re-render, meaning both of our messages show up at once.

StateHasChanged to the rescue

The solution, if we want to see “World” appear as soon as it’s added to the list, is to throw in an explicit StateHasChanged call.

_messages.Add("World");
StateHasChanged();

By calling StateHasChanged just after we add the “World” message, we force Blazor to re-render at this point, and our message appears straight away.

Conclusion

When invoking asynchronous methods in Blazor the UI will be rendered twice.

Once when the first await is encountered and again when the method completes.

To force the UI to re-render at any other point during the method’s execution you need to call StateHasChanged.

Join the Practical ASP.NET Newsletter

Ship better Blazor apps, faster. One practical tip every Tuesday.

I respect your email privacy. Unsubscribe with one click.

    Next up

    Finally! Improved Blazor Server reconnection UX
    .NET 9 changes how your Blazor Server app behaves when server connection is lost
    .NET 9 improves JavaScript module importing for Blazor
    .NET 9 ensures your users always get the latest version of your JS modules
    How to use .NET 9 to ensure users always get the latest version of your stylesheets
    .NET 9 changes how static files are served, and it solves a long-standing problem