skip to Main Content

I’ve got a component like this:

<h3>@IsEnabled</h3>

@code {
    [Parameter]
    public bool IsEnabled { get; set; }
}

And a page like this:

!!! OLD CODE, CHANGED CODE WITHOUT INFINITE LOOP BELOW !!!

@page "/"

<PageTitle>Binding Test</PageTitle>

<BlazorBindingTest.Components.MyComponent IsEnabled="@IsEnabled"></BlazorBindingTest.Components.MyComponent>

@code
{
    public bool IsEnabled { get; set; } = false;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            while (true)
            {
                await Task.Delay(1000);
                IsEnabled = !IsEnabled;
                //StateHasChanged(); // <<<<<<<<< Why is this neccessary?
            }
        }
    }
}

And I’ve noticed, that StateHasChanged seems to be neccessary in both, Blazor Server as well as WASM.
However, I’ve read that StateHasChanged can have significant negative impact on the whole rendering.

Why is it neccessary here and how to avoid it? Isn’t the component / the binding supposed to update automatically?

I’ve tried fields, properties, private, public, etc.


EDIT

The infinite loop was just an example to show the issue, I’ve modified the code as follows, and the issue naturally persists. This question is just about the need for StateHasChanged:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        _ = Task.Run(myInfinityLoop); // Do not await
    }
}

private async Task myInfinityLoop()
{
    while (true)
    {
        IsEnabled = !IsEnabled;
        await Task.Delay(1000);
        //StateHasChanged(); <<<< Still neccessary - Why?
    }
}

3

Answers


  1. Blazor detects changes automatically (property values or fields) when they are detected as user interaction, so a button click, etc. This is because Blazor has built-in event handlers to detect user interaction.

    However, when you update properties or fields outside of a UI event handler (like in your original example, where you were updating the IsEnabled property within an OnAfterRenderAsync method), Blazor doesn’t know that a change occurred, and you need to manually call StateHasChanged() to trigger a re-render.

    Generally, not recommended to change parameters inside OnAfterRenderAsync as it could lead to unintended forever loops.

    You could use EventCallback instead:

    // Parent Component
    @page "/"
    
    <PageTitle>Binding Test</PageTitle>
    
    <BlazorBindingTest.Components.MyComponent IsEnabled="@IsEnabled" OnToggleEnabled="@ToggleEnabled"></BlazorBindingTest.Components.MyComponent>
    
    @code
    {
        public bool IsEnabled { get; set; } = false;
    
        private void ToggleEnabled()
        {
            IsEnabled = !IsEnabled;
        }
    }
    
    // Child Component
    @code {
        [Parameter]
        public bool IsEnabled { get; set; }
    
        [Parameter]
        public EventCallback OnToggleEnabled { get; set; }
    
        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender)
            {
                while (true)
                {
                    await Task.Delay(1000);
                    await OnToggleEnabled.InvokeAsync();
                }
            }
        }
    }
    
    Login or Signup to reply.
  2. Why is it neccessary here and how to avoid it? Isn’t the component / the binding supposed to update automatically?

    Review the points below. You are running a process within the component class and changing it’s internal state: IsEnabled. There’s no component observer watching the internal state of the component, therefore you have to initialize an update process by calling StateHasChanged. Part of the Renderer’s process in handling that request is to check the state of sub component parameters. It calls SetParametersAsync on any sub component where the parameters may have changed. Note that the renderer can’t detect changes in objects, so it assumes they have changed.

    Points to consider:

    1. You should be very careful about what code you run in the AfterRender methods. [Personal] I would recommend only running JSInterop code unless you have to.
    2. Code in the lifecycle methods OnInitialized{Async}/OnParametersSet{Async}/OnAfterRender{Async} should run and complete [as quickly as possible]. Bad practice: You have an infinite loop which means that the first OnAfterRenderAsync never completes.
    3. Any UI event – button clicks, inputs, component EventCallbacks – call StateHasChanged automatically if a handler yields on an await, and on completion of handler execution.
    4. you should rarely need to call StateHasChanged. All too often it’s called to try and fix a logic problem in the code. Fix the logic and the need to call StateHasChanged goes away. see 5 for exceptions.
    5. You need to call StateHasChanged in standard event handlers (such as the timer elapsed event below).
    6. You need to call StateHasChanged when you update the component state and want to update the UI for the component and it’s siblings. There’s no observer watching on the internal component state and triggering a render if it changes.
    7. Unnecessary calls to StateHasChanged trigger render tree cascades. The more components in the tree, the higher the load.

    Here’s a slightly different implementation of your code that uses a timer to do your loop, with an event handler driving UI updates.

    MyComponent.razor

    <div class="@this.Css">@this.IsEnabled</div>
    
    @code {
        [Parameter, EditorRequired] public bool IsEnabled { get; set; }
    
        private string Css => this.IsEnabled ? "alert alert-success" : "alert alert-danger";
    }
    

    Index.razor

    @page "/"
    @using System.Timers
    @implements IDisposable
    
    <PageTitle>Index</PageTitle>
    
    <div class="m-2">
        <button class="@this.buttonCss" @onclick=this.Start>@this.buttonText</button>
    </div>
    
    <h1>Hello, world!</h1>
    
    <MyComponent IsEnabled=this.isEnabled />
    @code {
        private bool isEnabled { get; set; } = false;
        private string buttonCss => _timer.Enabled ? "btn btn-danger" : "btn btn-success";
        private string buttonText => _timer.Enabled ? "Stop" : "Start";
    
        private System.Timers.Timer _timer = new System.Timers.Timer(1000);
    
        protected override void OnInitialized()
        {
            _timer.AutoReset = true;
            _timer.Elapsed += this.OnTimerElapsed;
        }
    
        protected Task Start()
        {
            // No call to StateHasChanged needed as the UI Event Handler will do the calls automatically
            _timer.Enabled = !_timer.Enabled;
            return Task.CompletedTask;
        }
    
        // This invokes the handler on the UI Dispatcher context
        private async void OnTimerElapsed(object? sender, ElapsedEventArgs e)
            => await this.InvokeAsync(this.RefreshData);
    
        private Task RefreshData()
        {
            this.isEnabled = !this.isEnabled;
            // Need to call StateHasChanged as this is not a UI Event and there's no automatic calls to StateHasChanged
            // We call StateHasChanged directly here as the method is running in the Dispatcher context
            this.StateHasChanged();
            return Task.CompletedTask;
        }
    
        public void Dispose()
            => _timer.Elapsed -= this.OnTimerElapsed;
    }
    
    Login or Signup to reply.
  3. This second answer addresses some comments and issues in the updated question code. As this is a different issue, I’ve added a second answer rather than a very long single answer.

    Have you run this code with the StateHasChanged(); uncommented?

    [I’ve added a bit of debug code so you can see which thread you’re running on.]

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        Debug.WriteLine($"Current Thread:{Thread.CurrentThread.ManagedThreadId}");
        if (firstRender)
        {
            _ = Task.Run(myInfinityLoop); // Do not await
        }
    }
    
    private async Task myInfinityLoop()
    {
        while (true)
        {
            Debug.WriteLine($"Current Thread:{Thread.CurrentThread.ManagedThreadId}");
            IsEnabled = !IsEnabled;
            await Task.Delay(1000);
            StateHasChanged();
        }
    }
    

    Nothing will happen. If you check Outputyou will see:

    Exception thrown: 'System.InvalidOperationException' in Microsoft.AspNetCore.Components.dll
    

    Which highlights an issue in doing this:

    _ = Task.Run(myInfinityLoop);
    

    The exception occurs because you’ve switched to the thread pool by calling Task.Run, and then tried to run StateHasChanged outside the Despatcher context.

    Add the following extension to Task:

    public static class TaskExtensions
    {
        public static async Task Await(this Task task, Action? taskSuccess, Action<Exception>? taskFailure)
        {
            try
            {
                await task;
                taskSuccess?.Invoke();
            }
            catch (Exception ex)
            {
                taskFailure?.Invoke(ex);
            }
        }
    }
    

    And then

    //...
    <div hidden="@(!isError)" class="alert alert-danger">
        @errorMessage
    </div>
    
    //...
        private bool isError;
        private string? errorMessage;
    
        private void HandleFailure(Exception ex)
        {
            isError = true;
            errorMessage = ex.Message;
            this.InvokeAsync(StateHasChanged);
        }
    
        protected override Task OnAfterRenderAsync(bool firstRender)
        {
            Debug.WriteLine($"Current Thread:{Thread.CurrentThread.ManagedThreadId}");
            if (firstRender)
            {
               _ = Task.Run(myInfinityLoop).Await(null, HandleFailure);
            }
            return Task.CompletedTask;
        }
    

    And you’ll surface the exception, and be able to handle it.

    You can solve the exception like this:

        IsEnabled = !IsEnabled;
        await Task.Delay(1000);
        await this.InvokeAsync(StateHasChanged);
    

    I assume you’ve done this to "fire and forget" the loop from the lifecycle and let the lifecycle methods complete.

    There is a different way which doesn’t involve Task.Run.

        private Task _fireAndForget = Task.CompletedTask;
    
        protected override Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender)
                _fireAndForget = myInfinityLoop().Await(null, HandleFailure); 
    
            return Task.CompletedTask;
        }
    

    This simply assigns the task to a class variable. It will also solve the exception error, because now StateHasChanged will run in the Dispatcher context.

    You can then move the call into OnInitialized:

        protected override void OnInitialized()
        {
            _fireAndForget = myInfinityLoop().Await(null, HandleFailure);
        }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search