question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Add a way to prevent re-rendering after processing an `EventCallback` in Blazor

See original GitHub issue

Summary

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 and OnMouseMove 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 and ShouldRenderForEvent.
  • 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 ## Summary

Provide 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 the EventCallback do add complexity to the EventCallback 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:closed
  • Created 2 years ago
  • Reactions:1
  • Comments:16 (12 by maintainers)

github_iconTop GitHub Comments

2reactions
mrpmorriscommented, Apr 28, 2021

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.

1reaction
mrpmorriscommented, Jun 29, 2021

Added to 6.0-Preview4, but closed due to lack of duscussion @mkArtakMSFT

Read more comments on GitHub >

github_iconTop Results From Across the Web

Prevent refreshing the UI after an event in Blazor
The solution is prevent re-rendering the UI after an event is to provide a callback defined in a class that implement IHandleEvent ....
Read more >
ASP.NET Core Blazor performance best practices
To prevent rerenders for all of a component's event handlers, implement IHandleEvent and provide a IHandleEvent.
Read more >
How do I stop blazor @onclick re-rendering page
the main reason I want to stop this behavior is because I have a custom made grid component, it has a vertical scroll...
Read more >
Prevent @bind-Value re-rendering entire page : r/Blazor
Is there a way to prevent a particular binding variable update re-rendering the whole page, eg don't re-render the page if private int ......
Read more >
Avoid rerendering after handling events without state ...
In the main MS Blazor docs there is an article Avoid rerendering after handling events without state changesThey give an example of using...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found