aspnetcore: Add StateHasChanged(async: true) that guarantees never to run synchronously
The StateHasChanged method is supposed to flag the component to be re-rendered, so if you call this method multiple times from the same call, it should render the component only once.
Actually, this is working ok when the call is performed from a Blazor event callback.
If I have a component names Component1 and the following markup in Index.razor
@page "/"
<Component1 @ref="component1" />
<button class="btn btn-primary" @onclick="UpdateComponent">Update Component (From Blazor)</button>
@code {
Component1 component1;
private void UpdateComponent()
{
component1.UpdateTheComponent();
}
}
and the component code is the following
<h3>Component1</h3>
@{
System.Diagnostics.Debug.WriteLine("ComponentRendered");
}
@code {
public void UpdateTheComponent()
{
for (int i = 0; i < 100; i++)
{
StateHasChanged();
}
}
}
The text written in the output of visual studio is
“ComponentRendered”
Only one time.
If instead of calling the UpdateTheComponent() method from the Blazor button handler, it is called from JavaScript, the component is updated multiple times.
To call the UpdateTheComponent() method from javascript, I will alter the component to pass the component reference to a JavaScript method.
@inject IJSRuntime JS
<h3>Component1</h3>
<button @ref="button" class="btn btn-primary" onclick="window.Component1.updateComponent(this)">Update Component (From JS)</button>
@{
System.Diagnostics.Debug.WriteLine("ComponentRendered");
}
@code {
ElementReference button;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var ComponentReference = new Component1Reference(this);
await JS.InvokeVoidAsync("Component1.init", button, DotNetObjectReference.Create(ComponentReference));
}
}
public void UpdateTheComponent()
{
for (int i = 0; i < 100; i++)
{
StateHasChanged();
}
}
public class Component1Reference
{
private Component1 Component1;
internal Component1Reference(Component1 scrollViewer)
{
Component1 = scrollViewer;
}
[JSInvokable]
public void UpdateTheComponent()
{
Component1.UpdateTheComponent();
}
}
}
and the scripts.js javascript having the following
window.Component1 = {
init: function (elementReference, componentReference) {
elementReference.Component1 = componentReference;
},
updateComponent: function (element) {
element.Component1.invokeMethodAsync('UpdateTheComponent');
}
}
When I press the button, the javascript obtains the component reference and call to the Blazor method one time.
But this time, the component is rendered multiple times
ComponentRendered ComponentRendered ComponentRendered ComponentRendered ComponentRendered ComponentRendered ComponentRendered ComponentRendered ComponentRendered ComponentRendered ComponentRendered
In many scenarios this causes dramatic performance degradation, as the rendering is executed multiple times unnecessarily.
About this issue
- Original URL
- State: open
- Created 4 years ago
- Reactions: 13
- Comments: 57 (24 by maintainers)
@danroth27 In Gitter, you asked us to flag you on important issues that need attention. This is one of them. Any clues as to its position on the roadmap would be huge, thank you!
Hi @SteveSandersonMS
I too consider it a bug, simply because it is inconsistent. I think it should work the same whether it’s before or after an await, regardless of which approach is decided upon (multiple renders or a single render).
At the moment it is inconsistent and unexpected. It’s taken a long time for people to notice, and it will definitely trip people up.
I like the idea of having another method. But I think instead of a parameter named
asyncperhaps it should be something likebool forceImmediateRender?Maybe mark
StateHasChange()as[Obsolete]so that people will instead callStateHasChanged(bool forceImmediateRender). Over time you can remove the parameterless version and then defaultforceImmediateRendertofalse?Could you have a task that is completed once the render happens, and any calls marshalled from JS->C# will await that task before completing the JS promise and letting the JS code continue?
Same for unit testing, if it could access the queue then it too could await the task?
PS: Thanks for the interesting discussion 👍
@SQL-MisterMagoo from your example above, I think you found a simpler repro case of the bug. No Javascript involved.
In the code above the are 2 identical calls to the method UpdateTheComponent(). The first call is correctly executed and the second shows the bug. Lets see the log
When executing the first time, the StateHasChanged correctly flag the component and the render is correctly executed when the thread is released (because of the await Task.Delay). The second time the StateHasChanged executes the render directly which is the bug. The StateHasChanged should never execute the render directly, it should always flag it and queue it so the render is executed when the render thread is free.
In case anyone wants to justify this is by design, notice that component developers can’t expose api that reliably render the component without causing incomprehensible performance degradation. We should document in the api of every method that it must be called before any “await” otherwise the performance will suffer a bunch of repeated rendering. It is a complete nonsense. The platform must be reliable. This is a huge bug that must be fixed soon.
@arivoir Thanks for the additional information.
I see this is inconvenient in your case.
Overall I think there’s a mismatch of expectations here. You want
StateHasChangedto do something different from what it does in current releases. For back-compatibility reasons, I don’t think we would change the meaning ofStateHasChanged.Would it be fair to say that you are looking for some new API which means “schedule a render asynchronously but definitely don’t do it right now”? If so that’s a totally reasonable request, and we could definitely look at adding that in the future. It might look like this:
We could add a feature like that, but we wouldn’t take away the ability to trigger a synchronous render, as that is needed in cases like unit testing or certain JS interop scenarios.
In the meantime, component/app developers can work around it by adding that sort of API themselves. For example, on your component base class, add a method like this:
This is a bit of a fancy way of doing it (simpler ways are possible), but this way gives you back a
Taskthat you can optionallyawaiton the caller side if you want to know when it’s actually scheduled the render.So with this, you can replace calls to
StateHasChangedwithStateHasChangedAsyncand get the behavior you want, I hope!Does that sound reasonable? I know you want the default behavior to change, but like mentioned above, that’s almost certainly not something we’d do because of the back-compatibility issues and the fact that we do want there to be a way of starting the render batch synchronously.
We can look into adding a
StateHasChanged(async: true)method in the future if there’s community demand for it.I answered that above.
For example, if a complex UI subscribes to 10 pieces of state and all 10 update then it will cause 10 renders.
Calling StateHasChanged in a loop isn’t the goal, it just demonstrates that the component is being rendered immediately rather than simply being flagged.
For example, if a single operation causes multiple changes to shared state that a component is subscribed to then it will receive multiple notifications to rerender itself.
@arivoir I agree. I initially misunderstood your example and posted an erroneous response. Upon realising my error I deleted the comment 😃
The renderer dispatches the Blazor click event through Renderer.DispatchAsync, which sets _isBatchInProgress = true;
https://github.com/dotnet/aspnetcore/blob/c74e9efd0e6ea5b4d6737d23b92042772a50a249/src/Components/Components/src/RenderTree/Renderer.cs#L213-L256
This makes the render requests batch all 100 calls immediately.
Howevever, the codepath for JSInvokable does not go through Renderer.DispatchAsync, which makes the renderer set _isBatchInProgress only during the actual the actual processing of the batch, which is a direct result of the StateHasChanged() call. So the render calls are handled for every StateHasChanged() until MaxBufferedUnacknowledgedRenderBatches is hit in the RemoteRenderer. That’s after 10 render calls. The rest is simply dropped. I’m not sure whether this is intended behaviour and you just should not call StateHasChanged multiple times during JSInvoke, or that it’s a bug.
@SQL-MisterMagoo, all your reasoning is based on a mistake, it isn’t calling the StateHasChanged in an asynchronous method. It is synchronous. It’s a synchronous for, there is no await, therefore it shouldn’t execute the rendering multiple times. It should flag it and execute it just one time. Other behavior not obeying this is a major design failure in the platform and should be the priority 1 of the team because this affects the performance tremendously.
When developing any component with a relative degree of complexity, the ui can be invalidated for diverse causes, coming from different parts of the code, if the component can’t rely in the platform to invalidate the ui correctly without causing a massive numbers of renderings, the component itself needs to do it, which is frankly inviable.
Well, I really wasn’t expecting that. Good find!
FYI, I just ran into a situation where calling
StateHasChanged()caused some code to soft-deadlock (oddly, not there, but later on), and replacing it withawait StateHasChangedAsync()produced the desired effect both before and after. It’s a complex mesh of third-party libraries and JS interop, so I don’t have a concise analysis of where or why it got stuck. But this is perhaps indicative that this would be helpful to provide by default.I wrote this so my component wouldn’t update more than X times per second…
https://github.com/mrpmorris/Fluxor/blob/master/Source/Fluxor/UnsupportedClasses/ThrottledInvoker.cs
Actually, what would be really nice would be if there were two options in the parameters 1: Queue or RenderImmediately 2: Ignore or honour ShouldRender (Default / Forced)
That way when using something like Reactive patterns we could have a base component that always returns false from
ShouldRenderbut then when a reactive subscription fires the base component does a Queue + Forced render. That way we get optimised rendering (Queue) and only rendering when the state changes via RX.@SteveSandersonMS The StateHasChangedAsync workaround looks working, for some cases at least. Thanks for it.
How? The StateHasChanged is not reliable to perform a render synchronously. Is there more api to reliably perform a render?
IMO, the current StateHasChanged implementation is a bug. It’s inconceivable one method does one thing if you execute it either before or after an await call. I don’t imagine having to create documentation to explain every developer this intricate behavior when it should work just flagging the layout. Personally, it took me a lot of time realizing why the components were executing a lot of times unexpectedly. And there are more things that are affecting the rendering of our components, like event-callbacks. I’m looking to get rid of them because they also trigger the rendering unexpectedly. For example listening to a click performs a rendering.
Having a StateHasChangedAsync built-in in the platform is a must, also having a method to perform the render synchronously is a must. I understand your point about back-compatibility, but the current StateHasChanged should be discouraged to be used, an Obsolete attribute would explain developers what to use instead, and the documentation should say not to use it because of all the problems it brings.
Nobody said the rendering would be blocked for 10 seconds, that would be a terrible bug. The point is the second call should just perform 1 render.
I agree. I had thought of a timer as another case where a render is not the result of an event dispatch, but async is a nice find as well. This deserves a closer look quickly.
BTW, this has nothing to do with the fact that it’s async. Everything is executed on the current synchronizationcontext, there is no dispatching. You can even rewrite the UpdateComponent method as:
And it will still happen, even though we are sure now that StateHasChanged happens within the render context.
It’s a proof of principle.
In order to save processing time When calling StateHasChanged many times Then the component should only be marked as needing to be rerendered And should render only once when the next render process occurrs
Here is a more simple demonstration of the problem
In _Host.cshtml <head> add
Then click the button in the following component
It seems that neither void or async/await or
await Task.Delay(1)make any difference.@SQL-MisterMagoo StateHasChanged is called directly from a C# loop