Add a way to prevent re-rendering after processing an `EventCallback` in Blazor
See original GitHub issueSummary
Provide a mechanism that allows user to determine when re-rendering should not occur after an EventCallback
has been processed on a component.
Motivations and Goals
In a previous iteration of this design doc, the goal was to make it easier for developers to avoid re-rendering for events that are triggered frequently. The main motivation was to reduce the perf impact for customer scenarios. I played around with the following customer reported customer scenarios:
- Large tables with
OnScroll
event handlers. - Large tables with an
OnClick
handler in an element outside the table that modifies a single element. - Component with event handler that does not modify state.
From this, I came to the following conclusions.
- For some scenarios, the problem can be resolved by implementing better component best practices. For example, writing components that take primitive values to take advantage of the diffing logic.
- For scenarios like the
OnScroll
andOnMouseMove
event handlers, the solution would be better addressed via https://github.com/dotnet/aspnetcore/issues/10522. - The workarounds under “Current Approaches” are sufficient for most of the scenarios covered.
- In a few of the scenarios, perf impact can be reduced by leveraging new features like Virutalization and AoT.
With that in mind, I believe the goal should be to make it easier to.
- Users should be able to determine whether or not a re-render occurs for all events on a component.
- So far, I haven’t seen any scenarios where a single component has two events where it makes sense to not render for one but for the other.
- The implementation should not require any modifications to the Razor syntax.
- The API should be easier to use than existing appraoches. Easier here means fewer lines of code and less juggling of state.
- Users want to be able to re-render if the parameters provided to a component change but not if event handlers on it are called.
Current Approaches
The following approaches currently exist for solving the problem of “preventing render on event callbacks”
Use Action parameter for event handler
<button @onclick="@Handler">Click me</button>
@code {
[Parameter]
Action<MouseEventArgs> Handler;
}
Custom implementation of IHandleEvent
@implements IHandleEvent
<button @onclick="@HandleClick">Click me</button>
@code {
private void HandleClick() {
System.Console.WriteLine("Clicked...");
}
Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object arg)
=> callback.InvokeAsync(arg);
}
Provide an override for ShouldRender
Developers can implement a ShouldRender
override that returns false when required for their event handler.
<input @keydown="ProcessKeydown">
@code {
private bool shouldReload = true;
private void ProcessKeydown() {
shouldReload = false;
};
protected override void ShouldRender() {
if (shouldReload) {
return true;
}
return false;
}
}
In Scope
- Avoid changing too much of the existing event handling logic in
ComponentBase
- Avoid requiring users to roll out their implementations of native primitives (
ComponentBase
,IHandleEvent
)
Out of Scope
- Allow users to specify which event handlers should not trigger a re-render
Risks/Unknowns
- Risk: There might be some confusion around the existence of both a
ShouldRender
andShouldRenderForEvent
. - Risk: We want to avoid users using this feature as an escape hatch for best practices (e.g. using
Virtualization
).
Examples
In the example below, a re-render will be triggered in the MyEventCoponent
if the provided Text
parameter changes but not if the MyEventHandler
is invoked.
<h1>MyEventComponent</h1>
<p>@Text</p>
<button @onclick="MyEventHandler">Click me</button>
@code {
[Parameters]
public string Text { get; set; }
protected override bool ShouldRenderAfterEvent() {
return false;
}
private void MyEventHandler() {
System.Console.WriteLine("Event handler that does not change state.")
}
}
Detailed Design
With this in mind, I think the idea of adding a ShouldRenderAfterEvent
method to ComponentBase
is the most promising.
public abstract class ComponentBase {
+ protected virtual bool ShouldRenderAfterEvent()
}
Considered Alternatives
event:preventRender event handler
Expand this for the original proposed design for this issue
## SummaryProvide a mechanism for producing pure event handlers that do not trigger a render during event handling.
Motivation and goals
Currently, the ComponentBase
classes implementation of IHandleEvent.InvokeEventAsync
will call StateHasChanged
which triggers the re-render of the component (ref). This is generally the desired behavior but there are some scenarios where developers need to implement event handlers that do not re-render by default.
This is especially helpful for scenarios where:
- Re-rendering a component with a complex child tree can have a noticeable perf impact
- Developers want to avoid re-rendering on components where events are triggered frequently (e.g.
keydown
on large text inputs)
In scope
- Allow users to specify which event handlers should not trigger a re-render
Out of scope
- Supporting disabling re-rendering after event processing globally
- Avoid changing too much of the existing event handling logic in
ComponentBase
Risks / unknowns
- Mitigated risk: This behavior will be disabled by default and needs to be explicitly enabled by the user by setting
event:preventRender="true"
so the risk of confusing new developers who are used to the existing behavior is minimal. - Risk: It’s possible that users might assume that the
preventRender
attribute on a single event-handler - Risk: The changes required to add support for the
preventRender
flag in the compiler and propagate it through to theEventCallback
do add complexity to theEventCallback
feature. - Risk: Users need to be careful to only use
preventRender="true"
when intended or they run the risk of unexpected behavior (e.g. state inconsistent with UI) when their application is running.
Examples
In the example below, the preventRender
attribute is used to disable re-render on the component when #event-with-prevent-render is clicked.
Both buttons invoke IncrementCount
which does not modify component state. #event-with-render will still re-render even though no state is changed.
@page "/counter"
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount" @onclick:preventRender="true" id="event-with-prevent-render">Click me but don't render</button>
<button class="btn btn-primary" @onclick="IncrementCount" id="event-with-render">Click me and render</button>
@code {
private int currentCount = 0;
protected override void OnAfterRender(bool firstRender)
{
if (firstRender) {
System.Console.WriteLine("First render...");
} else {
// Should not happen when #event-with-prevent-render is called
System.Console.WriteLine("Rendering again...");
}
}
private void IncrementCount() {}
}
Detailed design
The implementation for this feature will be a two-step process with a change in the compiler and a change in the Blazor runtime.
To support this feature, the Razor compiler needs to be updated to produce the following generated code when a preventRender
attribute is included in a particular event handler.
<button class="btn btn-primary" @onclick="IncrementCount" @onclick:preventRender="true" id="event-with-prevent-render">Click me but don't render</button>
…will produce…
__builder.AddAttribute(7, "onclick", Microsoft.AspNetCore.Components.EventCallback.PureEventCallbackFactory.Create<Microsoft.AspNetCore.Components.Web.MouseEventArgs>(this, IncrementCount));
💡 : Note: this change needs to be populated to the Razor SDK for the future to fully light up.
The code above will utilize a new PureEventCallbackFactory
that produces EventCallback
s with their PreventRender
attribute set to true
.
Adjacent to this, we introduce a new internal property on the EventCallback
that is set via a new constructor.
public class EventCallback {
+ internal readonly bool PreventRender = false;
+ public EventCallback(IHandleEvent? receiver, MulticastDelegate? @delegate, bool preventRender) {}
}
Inside EventCallback.InvokeAsync
, we use PreventRender
to determine whether we want to call HandlePureEventAsync
or HandleEventAsync
on the receiver.
return PreventRender ?
Receiver.HandlePureEventAsync(new EventCallbackWorkItem(Delegate), arg)
Receiver.HandleEventAsync(new EventCallbackWorkItem(Delegate), arg);
The HandlePureEventAsync
method is a new method on the IHandleEvent
interface.
public interface IHandleEvent {
+ Task HandlePureEventAsync(EventCallbackWorkItem item, object? arg);
}
In ComponentBase
, we implement this HandlePureEventAsync
method that does not invoke StateHasChanged
after processing the event handler.
Task IHandleEvent.HandlePureEventAsync(EventCallbackWorkItem callback, object? arg)
{
...
- StateHasChanged();
...
}
PreventRender in EventArg
In the framework:
public class ComponentEventArgs : EventArgs
{
public bool PreventRender { get; set; }
}
// And now our built-in EventArgs types are changed to inherit from it, plus custom EventArgs types can optionally inherit from it:
public class MouseEventArgs : ComponentEventArgs
{
// ... same properties as before ...
}
In the component:
private void HandleSomeEvent(MouseEventArgs eventArgs)
{
eventArgs.PreventRender = true;
// ... and now the rest of your event logic ...
}
Issue Analytics
- State:
- Created 2 years ago
- Reactions:1
- Comments:16 (12 by maintainers)
I like the option of being able to disable auto render from events on a per component basis and having to call StateHasChanged.
But when there are lots of events and we only want to prevent one from rendering then disabling per event would be good… @onclick:NoRender
No rendering would take place for that event if disabled at component level or on the event.
Added to 6.0-Preview4, but closed due to lack of duscussion @mkArtakMSFT