🔰 Introduction
If you're building a general-purpose Razor component in Blazor, you've probably wanted to expose the DOM events happening inside it as the component's own event callbacks. The OnClick event is a classic example.
So, as a first try, we might implement a MyButton component like this:
<button @onclick="InternalOnClick">
Click me
</button>
@code {
[Parameter]
public EventCallback<MouseEventArgs> OnClick { get; set; }
private async Task InternalOnClick(MouseEventArgs args)
{
await OnClick.InvokeAsync(args);
}
}
I'll explain why in a moment, but the code above is actually a bad practice. At first glance, it looks a little wordy but otherwise fine, right? Well, not quite.
🤔 What Is the Problem?
The problem becomes serious once we start relaying high-frequency events, like the mouse hover event @onmouseover. Let's write that out:
<button @onclick="InternalOnClick"
@onmouseover="InternalOnMouseOver">
Click me
</button>
@code {
[Parameter]
public EventCallback<MouseEventArgs> OnClick { get; set; }
+ [Parameter]
+ public EventCallback<MouseEventArgs> OnMouseOver { get; set; }
private async Task InternalOnClick(MouseEventArgs args)
{
await OnClick.InvokeAsync(args);
}
+ private async Task InternalOnMouseOver(MouseEventArgs args)
+ {
+ await OnMouseOver.InvokeAsync(args);
+ }
}
Now, let's say the consumer of this MyButton component rarely wires up OnMouseOver:
<!-- OnMouseOver is sometimes captured,
but in most cases only OnClick is captured. -->
<MyButton OnClick="HandleClick" />
But here's the thing.
The MyButton component above always captures the @onmouseover DOM event, even when the caller never sets OnMouseOver. In Blazor WebAssembly, this may not be a huge deal. But in Blazor Server, every mouse move over the button triggers a round trip to the server. On mobile networks especially, that can really hurt performance. 😱
💡 How to Solve It
The fix is surprisingly simple. Just pass the component's event callback parameter directly to the DOM event, like this:
<button @onclick="OnClick"
@onmouseover="OnMouseOver">
Click me
</button>
@code {
[Parameter]
public EventCallback<MouseEventArgs> OnClick { get; set; }
[Parameter]
public EventCallback<MouseEventArgs> OnMouseOver { get; set; }
}
With this approach, when the caller doesn't set OnMouseOver, Blazor doesn't hook up the @onmouseover DOM event at all. No unnecessary server round trips. And as a bonus, we no longer need the awkward "capture in the component, then forward via InvokeAsync" dance. It's cleaner on every axis. 🎯
🔧 When We Want to Add Custom Logic Inside the Razor Component
Of course, sometimes we really do want to slip in between the DOM event and the component's callback, to run some extra logic inside the component. Unfortunately, as far as I can tell, this takes a bit more work.
First, we can no longer write the @onmouseover handler directly on the DOM element in the component's markup. The whole point is that we want to conditionally attach the handler, based on whether the caller actually set the OnMouseOver parameter.
To attach the handler dynamically, we'll use a dictionary together with the @attributes directive.
Here is the official Microsoft Learn documentation about @attributes.
The implementation so far looks like this:
<button @onclick="OnClick"
- @onmouseover="OnMouseOver"
+ @attributes="_ComponentParameters">
Click me
</button>
@code {
[Parameter]
public EventCallback<MouseEventArgs> OnClick { get; set; }
[Parameter]
public EventCallback<MouseEventArgs> OnMouseOver { get; set; }
+ private readonly Dictionary<string, object> _ComponentParameters = [];
}
Next, for the handler that runs our custom logic, we create an EventCallback object during initialization:
@code {
// ... omitted ...
+ private EventCallback<MouseEventArgs> _internalOnMouseOver;
+ protected override void OnInitialized()
+ {
+ _internalOnMouseOver = EventCallback.Factory.Create<MouseEventArgs>(this, InternalOnMouseOver);
+ }
+ private async Task InternalOnMouseOver(MouseEventArgs args)
+ {
+ // Here, we run our extra custom logic, and then call the component's event callback.
+ await OnMouseOver.InvokeAsync(args);
+ }
}
Finally, we override OnParametersSet. Based on whether OnMouseOver has actually been set, we add or remove the onmouseover entry in _ComponentParameters:
@code {
// ... omitted ...
protected override void OnParametersSet()
{
base.OnParametersSet();
if (OnMouseOver.HasDelegate)
{
_ComponentParameters["onmouseover"] = _internalOnMouseOver;
}
else
{
_ComponentParameters.Remove("onmouseover");
}
}
}
With this setup, Blazor only captures the @onmouseover DOM event when the caller actually wires up OnMouseOver. And we still get to run our own logic before and after the event. 👍
📝 A Note About EventCallback.Empty
You might wonder: what if I just set EventCallback.Empty as a default? Sadly, that doesn't help. Even with EventCallback.Empty, Blazor still captures the event. So in Blazor Server, assigning EventCallback.Empty to @onmouseover still triggers a server round trip every time the mouse moves over the element.
EventCallback.Empty does not mean "don't capture the event". It means "capture the event and run an empty handler". So we cannot use it as a trick to skip capturing DOM events.
That said, what if we really do have to pass some value to the event handler directive, but without actually subscribing to the event? Good news. In that case, we can pass default(EventCallback) instead of EventCallback.Empty. With default(EventCallback), Blazor will skip capturing the DOM event, exactly as we want. 🙌
🎉 Summary
Here is what I took away from all of this:
- The "capture the DOM event inside the component and forward it via
InvokeAsync" relay pattern is a bad practice for general-purpose Razor components. The component keeps capturing the DOM event even when the caller never wires up the callback, which can flood Blazor Server with unnecessary traffic for frequent events like@onmouseover. - The right approach is to bind the component's
EventCallbackparameter directly to the DOM element's event attribute. When the caller does not set it, Blazor simply will not capture the DOM event. - If we really want to run custom logic inside the component before or after the event, we can use
@attributeswith aDictionary<string, object>, and toggle entries inOnParametersSetbased onHasDelegate.
Keeping these points in mind, let's build efficient Blazor components that don't flood the server with unnecessary traffic.
If you have any other tips or experiences, feel free to share them in the comments! 👇
❤️ Happy coding!
Top comments (0)