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 theOrderDeleted
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.
Build Better Blazor Web Apps, Faster