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
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 anOnAfterRenderAsync
method), Blazor doesn’t know that a change occurred, and you need to manually callStateHasChanged()
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: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 callingStateHasChanged
. Part of the Renderer’s process in handling that request is to check the state of sub component parameters. It callsSetParametersAsync
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:
OnAfterRenderAsync
never completes.StateHasChanged
automatically if a handler yields on an await, and on completion of handler execution.StateHasChanged
. All too often it’s called to try and fix a logic problem in the code. Fix the logic and the need to callStateHasChanged
goes away. see 5 for exceptions.StateHasChanged
in standard event handlers (such as the timer elapsed event below).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.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
Index.razor
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.]
Nothing will happen. If you check
Output
you will see:Which highlights an issue in doing this:
The exception occurs because you’ve switched to the thread pool by calling
Task.Run
, and then tried to runStateHasChanged
outside the Despatcher context.Add the following extension to Task:
And then
And you’ll surface the exception, and be able to handle it.
You can solve the exception like this:
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
.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
: