Need your Blazor sibling components to talk to each other?

April 18, 2023 · 7 minute read · Tags: blazor

We’ve just come out of the Easter school holiday here in the UK and I can confirm, with a great deal of authority, that siblings don’t always get along…

It’s true for human siblings (naturally) but also for your Blazor components.

Take this example:

We have a user dashboard, with two child components:

  • One to show some basic details about the user (their profile)
  • Another to show orders they’ve placed in a fictional online store

Notice how the two components sit next to each other in the component tree.

At the moment these two children components are fetching their own data.

The challenge comes when a change in one child needs to trigger a change in the other.

Say, for example, we are able to cancel an order from the UserOrders component, and we want the UserProfile to be updated (to show the correct number of open orders).

UserOrders can’t directly communicate with UserProfile to tell it to update, for good reason.

Apart from anything else, we could be using UserProfile in multiple places, so any attempt to directly locate and access all instances of it would soon get messy.

So how do we make UserProfile update when an order is deleted via UserOrders?

Two options spring to mind:

  • Use callbacks and “lift state up”
  • Use a service which sits outside the component tree to “broker” communication between components

Let’s look at each in turn.

Lift state up

The first, and possibly simplest option, is to lift state up to the parent component.

If we make the parent UserDashboard component responsible for fetching the data needed for the user profile, we can control when that data gets refreshed.

Here’s a modified version:

There are a few changes here:

  • We’ve modified UserOrders to raise a callback when an order is deleted
  • UserProfile now accepts its data via a parameter (rather than fetching it itself)
  • UserDashboard fetches the summary data once when the page loads, and again every time the OrderDeleted callback is invoked

Callbacks are the primary way we can “signal up” to the parent component that something noteworthy has happened.

UserOrders.razor

@code {

    [Parameter]
    public EventCallback<int> OnDelete { get; set; }
    
    ...

    void Delete(int orderId)
    {
        OrderDB.Delete(orderId);
        OnDelete.InvokeAsync(orderId);
    }

}

The parent UserDashboard component can handle that callback and fetch new data from the backend API/DB, then push that back “down” the tree to UserProfile.

UserDashboard.razor

@code {

    private UserSummary? _summary;

    protected override void OnInitialized()
    {
        RefreshSummary();
    }

    private void RefreshSummary()
    {
        _summary = OrderDB.GetSummaryForUser();
    }

}

As a result, UserProfile is much simpler, it simply accepts summary data via a parameter (and doesn’t have to fetch data itself).

UserProfile.razor

@code {

    [Parameter]
    public UserSummary? Summary { get; set; }

}

Use a service to “broker” communication between components

Another choice here is to use a separate service to handle the communication between components.

There’s a little bit going on here so let’s break it down.

First, we have a new service called UserDashboardService:

public class UserDashboardService
{
    public Action OnOrderDeleted { get; set; }

    public void NotifyOrderDeleted()
    {
        OnOrderDeleted?.Invoke();
    }
}

Components can register a handler for the OnOrderDeleted Action which will then be invoked every time NotifyOrderDeleted is called.

UserDashboard no longer has to concern itself with fetching state, or passing data down to either of the children components.

UserOrders takes the UserDashboardService as a dependency, and calls its NotifyOrderDeleted method when an order is deleted:

UserOrders.razor

@code {
   
    ...
    
    void Delete(int orderId)
    {
        OrderDB.Delete(orderId);
        UserDashboardService.NotifyOrderDeleted();
    }
    
    ...
    
}

Finally UserProfile also takes the UserDashboardService as a dependency and registers a handler for the OnOrderDeleted action.

When OnOrderDeleted is invoked the RefreshSummary method will run to load the up-to-date user order summary data.

UserProfile.razor

@code {

    private UserSummary? _summary;

    protected override void OnInitialized()
    {
        UserDashboardService.OnOrderDeleted += RefreshSummary;
        RefreshSummary();
    }

    private void RefreshSummary()
    {
        _summary = OrderDB.GetSummaryForUser();
        StateHasChanged();
    }

    public void Dispose()
    {
        UserDashboardService.OnOrderDeleted -= RefreshSummary;
    }

}

Note we call StateHasChanged here.

Because we’re handling this communication outside of the component tree we need to nudge Blazor to re-render.

This also implements IDisposable and unregisters the handler for UserDashboardService.OnOrderDeleted when this component is disposed. This is just to ensure we don’t inadvertently create a memory leak.

Which is better?

So which of these two approaches is better?

Well, as ever, it depends!

Sometimes it makes sense for the parent component to take control of the data, and push that data down to the children.

We get to limit the number of components with side effects (calling a backend API/DB) giving us simpler children components which take data via parameters and have their own internal logic for what they do with it.

If we’re essentially modelling a page, which is a cohesive part of our UI, and the child components are only there to enable us to break the UI down into smaller, more manageable components, then lifting the state up is probably the way to go.

But sometimes components need control of their own data. For example, you can imagine a component which uses a datagrid to show data.

There’s a good chance you want this component to fetch its own data, not least so you can handle things like pagination, sorting, filtering, etc.

In that case, a service which sits outside the component tree and “brokers” communication between components is a good choice.

You can make your child component nice and efficient, with efficient calls to the DB/backend to fetch data, but still have it respond to ‘signals’ from elsewhere in the application.

In Summary

Sometimes you need your sibling components to communicate with each other.

Blazor’s design encourages you to keep components self-contained, and to avoid tight coupling between components.

As a result, you can’t can’t make direct calls between sibling components.

But you can use callbacks to signal up the component tree, and then push data back down.

Or you can use a separate service to raise an action when something happens, and have components register their own handlers for that action.

Thinking in components

This shift to “thinking in components” is one of the toughest challenges when moving to Blazor from a more traditional “page based” framework (like MVC, Razor Pages, WebForms).

There are a few core concepts to learn, but master these and you’ll discover you can build out your features much faster (and with much less technical debt) when you use components to their full potential.

In the next few weeks I’m launching Practical Blazor Components, a new self-paced interactive workshop that covers these and other useful patterns for handling component communication.

Check out the link below for all the details.

Web development should be fun.

Write code, hit F5, view it in the browser and bask in the glory of a job well done.

But you're not basking… Why aren't you basking?!

Cut through all the noise and build better, simpler Blazor web applications with Practical Blazor Components.

Practical Blazor Components Build Better Blazor Web Apps, Faster

Next up

Interactive what now? Deciphering Blazor’s web app project template options
Create a new Blazor Web App and you’ll be asked how you want interactivity to work, but what does it all mean?
Should I put my Blazor components in the server project, or the client project?
.NET 8 gives you a choice of projects, which one should you use?
Share user authentication state with interactive WASM components
Your server knows your user is authenticated, but what about your interactive WASM components?