async-awaitblazorrenderingblazor-webassembly

When does a blazor component render?


In the diagram below from this page it shows that when an incomplete task is returned by the call to OnInitializedAsync it will await the task and then render the component.

However it seems that what actual happens when an incomplete task is returned is renders the component immediately, and then renders it again once the incomplete task completes.

enter image description here

An example later in the page seems to confirm this. If the component was not rendered immediately after the call to OnInitializedAsync, and instead only rendered for the first time after the Task returned had been completed you would never see the "Loading..." message.

OnParametersSetAsync behavior appears the same. It renders once immediately when an incomplete task is returned, and then again once that task has completed.

Am I misunderstanding the render lifecycle, or is this an error in the documentation?

Thanks

@page "/fetchdata"
@using BlazorSample.Data
@inject WeatherForecastService ForecastService

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from a service.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <!-- forecast data in table element content -->
    </table>
}

@code {
    private WeatherForecast[]? forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
    }
}

Solution

  • To fully answer your question we need to delve into the ComponentBase code.

    Your code is running in the async world where code blocks can yield and give control back to the caller - your "incomplete task is returned".

    SetParametersAsync is called by the Renderer when the component first renders and then when any parameters have changed.

    public virtual Task SetParametersAsync(ParameterView parameters)
    {
        parameters.SetParameterProperties(this);
        if (!_initialized)
        {
            _initialized = true;
            return RunInitAndSetParametersAsync();
        }
        else
            return CallOnParametersSetAsync();
    }
    

    RunInitAndSetParametersAsync is responsible for initialization. I've left the MS coders' comments in which explains the StateHasChanged calls.

    private async Task RunInitAndSetParametersAsync()
    {
        OnInitialized();
        var task = OnInitializedAsync();
    
        if (task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled)
        {
            // Call state has changed here so that we render after the sync part of OnInitAsync has run
            // and wait for it to finish before we continue. If no async work has been done yet, we want
            // to defer calling StateHasChanged up until the first bit of async code happens or until
            // the end. Additionally, we want to avoid calling StateHasChanged if no
            // async work is to be performed.
            StateHasChanged();
            try
            {
                await task;
            }
            catch // avoiding exception filters for AOT runtime support
            {
                if (!task.IsCanceled)
                    throw;
            }
            // Don't call StateHasChanged here. CallOnParametersSetAsync should handle that for us.
        }
        await CallOnParametersSetAsync();
    }
    

    CallOnParametersSetAsync is called on every Parameter change.

    private Task CallOnParametersSetAsync()
    {
        OnParametersSet();
        var task = OnParametersSetAsync();
        // If no async work is to be performed, i.e. the task has already ran to completion
        // or was canceled by the time we got to inspect it, avoid going async and re-invoking
        // StateHasChanged at the culmination of the async work.
        var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
            task.Status != TaskStatus.Canceled;
    
        // We always call StateHasChanged here as we want to trigger a rerender after OnParametersSet and
        // the synchronous part of OnParametersSetAsync has run.
        StateHasChanged();
    
        return shouldAwaitTask ?
            CallStateHasChangedOnAsyncCompletion(task) :
            Task.CompletedTask;
    }
    

    In the diagram substitute "Render" for StateHasChanged in the code above.

    The diagram uses the work "Render", which is a bit misleading. It implies that the UI re-renders, when what actually happens is a render fragment (a block of code that builds the UI markup for the component) is queued on the Renderer's render queue. It should say "Request Render" or something similar.

    If the component code that triggers a render event, or calls StateHasChanged, is all synchronous code, then the Renderer only gets thread time when the code completes. Code blocks need to "Yield" for the Renderer to get thread time during the process.

    It's also important to understand that not all Task based methods yield. Many are just synchronous code in a Task wrapper.

    So, if code in OnInitializedAsync or OnParametersSetAsync yields there's a render event on the first yield and then on completion.

    A common practice to "yield" in a block of synchronous code is to add this line of code where you want the Renderer to render.

    await Task.Delay(1);
    

    You can see ComponentBase here - https://github.com/dotnet/aspnetcore/blob/main/src/Components/Components/src/ComponentBase.cs