In Blazor, data flows down through parameters. But components also need to communicate up — a button inside a card signals the parent to remove it, a form signals its parent to save. The mechanism for this is EventCallback<T>.

It is not just syntactic sugar. EventCallback<T> automatically triggers a re-render on the parent, is awaitable, and handles the synchronisation context for you. These three properties make it the only correct choice for child-to-parent communication.

Why a Child Cannot Call Parent Methods Directly

The parent renders the child. The child does not have a reference to the parent object — and it should not, because that would create a circular dependency that makes components impossible to reuse. Instead, the parent passes a callback down as a parameter, and the child calls it when something happens.

EventCallback<T> — Declaration, Invocation, and Receiving

A full example with an ItemCard child and a parent list:

@* ItemCard.razor *@
<div class="item-card @(IsSelected ? "selected" : "")">
    <h3>@Item.Name</h3>
    <p>@Item.Description</p>
    <button @onclick="HandleSelect">Select</button>
    <button @onclick="HandleDelete">Delete</button>
</div>

@code {
    [Parameter, EditorRequired] public Item   Item       { get; set; } = default!;
    [Parameter]                 public bool   IsSelected { get; set; }
    [Parameter]                 public EventCallback<Item> OnSelect { get; set; }
    [Parameter]                 public EventCallback<Item> OnDelete { get; set; }

    // Convention: private handler calls the public callback
    private async Task HandleSelect() => await OnSelect.InvokeAsync(Item);
    private async Task HandleDelete() => await OnDelete.InvokeAsync(Item);
}
@* ItemList.razor (parent) *@
@foreach (var item in items)
{
    <ItemCard Item="item"
              IsSelected="selectedItem?.Id == item.Id"
             
              />
}

@if (selectedItem is not null)
{
    <p>Selected: @selectedItem.Name</p>
}

@code {
    private Item? selectedItem;

    private void HandleItemSelected(Item item) => selectedItem = item;
    private void HandleItemDeleted(Item item)  => items.Remove(item);
}

When the user clicks Select, the child invokes OnSelect with the item. The parent's HandleItemSelected runs, updates selectedItem, and Blazor re-renders the parent — and therefore all child cards, so the correct one is highlighted. This happens with zero manual calls to StateHasChanged().

Multiple Callbacks on One Component

There is no limit on the number of EventCallback parameters. Name them with the On prefix to match the Blazor convention and make the parent's template self-documenting:

[Parameter] public EventCallback<Item> OnSelect  { get; set; }
[Parameter] public EventCallback<Item> OnDelete  { get; set; }
[Parameter] public EventCallback<Item> OnArchive { get; set; }
[Parameter] public EventCallback       OnRefresh { get; set; } // no payload

The IsSelected Pattern — State Flows Back Down as a Parameter

Notice that IsSelected is a [Parameter] bool, not a field owned by the child. The parent computes it (selectedItem?.Id == item.Id) and passes it in. This is the correct one-way data flow pattern: the parent owns the selection state; the child displays whatever the parent tells it. The child never stores "am I selected?" locally — that would break the single source of truth.

EventCallback<T> vs Action<T> — Full Comparison

Feature EventCallback<T> Action<T>
Triggers parent re-render automatically Yes No — you must call StateHasChanged manually
Awaitable Yes No
Synchronisation context Handled internally Caller's responsibility
Null-safe by default Yes — InvokeAsync is a no-op when unset No — must null-check before invoking
Correct for Blazor components Always Never

The practical consequence of the re-render difference: if you use Action<T>, the parent's state updates but the UI does not — until some other event forces a render. This produces a class of bugs where clicking a button appears to do nothing on the first click.

Key Takeaways

  • EventCallback<T> is the only correct mechanism for child-to-parent communication in Blazor.
  • It automatically triggers a re-render on the parent after invocation — no manual StateHasChanged() needed.
  • It is awaitable — the parent handler can be an async Task and the child can await the invocation.
  • InvokeAsync is a no-op when the callback is not set — no null checks required in the child.
  • Never use Action<T> for component callbacks — it does not trigger re-renders and is not awaitable.
  • Keep selection state in the parent and pass it back down as a bool parameter — the child never owns shared state.