Search code examples
c#.netasync-awaitblazor-webassembly

Calling StateHasChanged vs returning Task


I'm working with the basic template for a Blazor web assembly project. I am trying to understand what's going on in two different approaches that seem to reach the same result.

In my first attempt, I created a method that would post a new WeatherForecast model to my web api endpoint and then from the response, add that newly created WeatherForecast to a list. Here is that first method:

private async void ValidSubmitHandler()
{
    var response = await Http.PostAsJsonAsync<WeatherForecast>(Configuration["APIBaseUrl"] + "/WeatherForecast", newForecast);

    var forecast = await response.Content.ReadFromJsonAsync<WeatherForecast>();
    forecasts.Add(forecast);
}

I was expecting the UI to update with the newly added forecast but instead the page didn't update despite the forecasts list having the new object added. I put a breakpoint and observed that the new forecast was indeed added to the list but just the UI was not rendering the change to the list's state.

I read that you can call

StateHasChanged() 

to initiate the change to render the list's new forecast and for the moment that was fine. After reading around a bit about StateHasChanged it seems like I shouldn't use that very often and it would show signs of bad practices so I tried a suggestion from a colleague who said I need to add Task as the return type rather than void to the function like so:

private async Task ValidSubmitHandler()
{
    var response = await Http.PostAsJsonAsync<WeatherForecast>(Configuration["APIBaseUrl"] + "/WeatherForecast", newForecast);

    var forecast = await response.Content.ReadFromJsonAsync<WeatherForecast>();
    forecasts.Add(forecast);
}

This indeed did allow for the UI to pick up on the updates and I can only assume that ValidSubmitHandler() was exiting before the new forecast was added to the list.

Does anyone have an explanation as to why StateHasChanged seemed to work if my method was not awaiting the results from the async operations?

thanks


Solution

  • Blazor automatically call StateHasChanged when the event handler completes. Here's the relevant code from GitHub:

        // https://github.com/dotnet/aspnetcore/blob/547de595414d6ebb9ddeaa4a231815449c9f3c60/src/Components/Components/src/ComponentBase.cs#L301
        Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
        {
            var task = callback.InvokeAsync(arg);
            var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
                task.Status != TaskStatus.Canceled;
    
            // After each event, we synchronously re-render (unless !ShouldRender())
            // This just saves the developer the trouble of putting "StateHasChanged();"
            // at the end of every event callback.
            StateHasChanged();
    
            return shouldAwaitTask ?
                CallStateHasChangedOnAsyncCompletion(task) :
                Task.CompletedTask;
        }
    

    Blazor waits for the event handler to return. Then, it calls StateHasChanged. If the returned Task is not completed, it awaits the task and call StateHasChanged again.

    In the case of async void, the returned task is always completed, so it will never await it and call the second StateHasChanged. In this case, you would need to manually call it. But a better way is to avoid async void which are almost always a bad thing.

    I've also written a more in-depth blog post about component lifecycle ASP.NET Core Blazor components lifecycle