3+1 ways to manage state in your Blazor application
June 23, 2020 · 12 minute read · Tags: blazor
Blazor is all about components, but let’s face it, one solitary component is of limited use.
Much like you or I, one component can only achieve so much by itself.
The real magic happens when you bring components together.
Entire applications spring into life when you compose several components together to create larger features.
But, as soon as you have more then one component you need a way to manage the data that flows between them.
Get this wrong and, what started off as a “simple enough UI” can slowly devolve into a multi-layered, highly coupled mess, where even the smallest change has the potential to unravel everything.
Thankfully we can just as easily avoid such chaos, if we take a little time to understand the tools Blazor gives us for managing state in our application…
Let’s take a really simple example, a simple component to display the logged in user’s name…
Option 1: Don’t pass anything (aka fetch your own data!)
First up we have the decentralised model…
Why go to all the effort of passing state around when you can just let each component fetch its own?
The UserName component makes its own calls to the API, fetches the details for the current user, then renders their name.
@inject HttpClient Http
<span>@_profile.Name</span>
@code {
private Profile _profile;
protected override async Task OnInitializedAsync()
{
_profile = await Http.GetFromJsonAsync<Profile>("/profile");
}
}
Pros
- Straightforward to implement
- Easy to reason about (you can look at the component and quickly establish what it does)
- Plug and play (this component can literally be dropped anywhere in your app and it “just works”)
Cons
- Multiple instances == multiple network calls
- This component relies on data being stored/persisted in the backend
- Components know about data persistence/retrieval
- Gets rapidly more complex in non read-only scenarios (persisting data)
- Hard to make components react to state changes, without getting them to periodically re-fetch data for themselves
Taking this approach assumes your components will be fetching data from a backend API but this raises some questions.
What if you have state in your application that you don’t want to persist (or want to delay persisting). For example, your user might be filling in a form and you want to keep hold of it until they submit it to the backend.
The broader issue here is, should components fetch their own data?
What if you start storing more data “in memory”, or you decide to use local storage instead of API calls?
If every component fetches its own data these adjustments can quickly spiral into lots of breaking changes required for your existing components.
In a more complex example you might also end up with several components making the same (or similar) API calls to /Profile
. This might be OK, but doesn’t seem very efficient, especially if /Profile
is a call which returns large amounts of data…
Overall, this approach works just fine for small apps, and may be all you need.
But be aware it’s not right for every job, presents some challenges in a larger app and doesn’t always scale particularly well….
Option 2: Pass state down, all the way down
Probably the “simplest” and certainly most obvious alternative to each component fetching its own data, is to pass state all the way “down the tree”; each component taking the message and passing it along.
Here Blazor shares some similarity with the approach taken by React.
In React state flows down, and events bubble back up…
In this case, NavBar
takes on responsibility for fetching profile details, then passes them along to every other component in the chain until they get to UserName
.
NavBar
@inject HttpClient Http
<LoggedInUser profile="@_profile"/>
@code {
private Profile _profile { get; set; }
protected override async Task OnInitializedAsync()
{
_profile = await Http.GetFromJsonAsync<Profile>("/profile");
}
}
NavBar
grabs the data from the API, then renders an instance of LoggedInUser
, passing the _profile
along for the ride.
LoggedInUser
<UserName profile="@Profile"/>
@code {
[Parameter]
public Profile Profile { get; set; }
}
LoggedInUser
takes an object of type Profile
as a parameter, but in this case doesn’t need it for itself, just forwards it on to an instance of UserName
.
UserName
<span>@Profile.Name</span>
@code {
[Parameter]
public Profile Profile { get; set; }
}
UserName
finally gets its data and renders the user’s name!
Pros
UserName
is now data-persistence agnostic- It can still be used in different parts of the app (but does require an instance of the
Profile
object) - Easier to test (just pass data in via the parameter, no need to hit a real database or mock anything out)
Cons
- There are several links in the chain from the top component to the bottom (potentially many more than in this simple example)
- Each link can end up passing data along (and taking data in) which it doesn’t actually need itself
- Adds boilerplate code to all the components in the middle of the chain
- Navigating the code (to establish where some data comes from) takes time
- Adding new parameters to components at the “bottom” of the tree, requires sending data all the way down from the top
With this option, we now have a UserName
component which isn’t directly tied to the backend. It can go anywhere, so long as we can furnish it with an instance of Profile
.
We can therefore minimise the number of network calls to/Profile
, potentially only calling it once at the “top” of the component tree, reducing overall network calls and bandwidth.
However, in any realistically large application we’re likely to spend a significant amount of time navigating up and down the component tree because it’s not obvious (from looking at any individual component) where its data came from.
Mind the gap
Say we decide to include the date and time the user last accessed the system in our UserName
component.
We’d have to figure out where the Profile
instance used by UserName
originates, and make sure we populate it with the correct data in the first place, before we can be sure LastLogin
will work.
The bigger the gap between where we need to show the new data and where we go to populate it, the more work for us to track down the relevant code.
We also end up adding boilerplate code to all the components in the “middle” of this chain even if they don’t need access to Profile
themselves (they still have to accept it and pass it on).
This problem becomes especially obvious when you need to pass additional parameters down the tree.
Imagine for example we declared an additional parameter in UserName
, say a ShowLastLogin
flag to control whether to show the user’s last login date/time.
We’d have to pass that all the way in from the top, changing lots of components as we go, just to get the correct value down to the bottom of the tree.
If we’re not careful, we’re going to end up passing a lot of parameters around, updating many components every time we add something new, just to get a value to show up at the bottom of the tree.
Option 3: Cascading Values - an alternative
Blazor has another option for getting state to a component.
Take our profile details for example; it’s quite feasible we’d want to access that information from several different components.
- Login Control
- User Avatar
- User Name
So what should we do? Should we pass the profile down into each one, through as many components as it takes to get there?
We could, but this has the potential to turn into a maintenance nightmare.
Instead we can declare a cascading value at the “top” of our application, then access that parameter anywhere we like in the component tree below it.
To make this work, we declare the cascading value…
NavBar
@inject HttpClient Http
<CascadingValue Name="Profile" Value="@_profile">
<LoggedInUser />
</CascadingValue>
@code {
private Profile _profile { get; set; }
protected override async Task OnInitializedAsync()
{
_profile = await Http.GetFromJsonAsync<Profile>("/profile");
}
}
Now any component “inside” this CascadingValue
element, will have direct access to the Profile…
UserName
@if (Profile != null)
{
<span>Hey @Profile.Name!</span>
}
@code {
[CascadingParameter(Name="Profile")]
private Profile Profile { get; set; }
}
It’s generally a good idea to check for nulls in this scenario, otherwise you’d get a nasty error before the Profile
cascading value is set.
So long as our CascadingParameter
references the correct name, we’ll have access to the Profile.
LoggedInUser
<UserName />
The big advantage is LoggedInUser
no longer has to handle Profile
just because UserName
needs it further down the tree, considerably reducing the amount of code!
Pros
- Avoids components having to know about values which they’re not using themselves (but would otherwise need to pass on)
- Reduces the amount of code
- Particularly useful for “global” state such as user profiles (which many components may require access to)
- Decouples components from the component tree (you can render the component anywhere so long as it still has access to the cascading value)
Cons
- You have to locate the “other end” of the cascading value to figure out where values came from
- Add too many cascading values and everything starts to feel like magic!
Cascading values are most useful for state which might be used by lots of components.
Broadly speaking this would include “application” concerns like the logged in user’s details, user preferences etc.
Use them too much however and it becomes pretty difficult to maintain your code as each use of CascadingParameter
requires a bit of hunting to track down the “other end”, where the actual value is set.
Another option - next level state management!
The chances are, for many “simple” web applications you’ll find a combination of the three options we’ve explored here more than sufficient.
It really, really pays to keep things simple.
Unless you have many nested components, the cost of passing data between your components may well stay low enough that you don’t need to look to any other solutions.
But, as applications grow in scope, they often grow in complexity and at that point you might start to feel like you need a different option.
Centralised state
This brings us to a concept which has found favour in javascript land; building a centralised store for your state.
Libraries such as Redux provide a client-side store which represents the current state of the application for that particular instance (running in the browser).
With this model your components can subscribe to this centralised state, react to changes (showing up-to-date data) and invoke actions which cause the state to change.
There are a few emerging “off the shelf” options for implementing this kind of state management in Blazor:
It’s worth keeping an eye on Awesome Blazor to see if any more crop up over the coming weeks and months.
Roll your own centralised state
Libraries like these bring their own abstractions and learning curves to the party, and can be a little tricky to pick up.
The other option is to roll your own service for storing UI state.
With this “poor man’s state management” approach you can hold data in a class which stays in memory for your Blazor application.
Multiple components can then bind to this object and display the profile details as they see fit.
The simplest way to do this is to create a class and register it using Microsoft’s Dependency Injection.
public class ProfileService
{
public Profile Profile { get; private set; }
public async Task Set(Profile profile)
{
Profile = profile;
}
}
Here we’ve a standard C# class which exposes a public property for the current state of Profile
and a method to update the Profile with different data.
We can register this as a Singleton (if we’re using Blazor Web Assembly, in program.cs).
builder.Services.AddSingleton<ProfileService>();
Now when the user accesses this in the browser they will get a single instance of this service until they refresh their browser.
Anytime we want to display profile details in any component we can bind to it…
@inject ProfileService ProfileService
<h1>Hey @ProfileService.Profile.Name</h1>
And here’s how we can update the profile:
@inject ProfileService ProfileService
<h1>Hello World!</h1>
@code {
protected override async Task OnInitializedAsync()
{
await ProfileService.Set(new Profile { Name = "Jon" });
}
}
But there is one teeny tiny flaw in this…
If we were to call ProfileService.Set()
from one component, other components in our app which are bound to the profile won’t automagically re-render to show the updated value…
In this example, we have a page where users can edit their profile (change their name etc.)
Ideally we’d want everywhere in our application which displays profile information to be updated immediately when the profile is modified and saved.
However, as it stands calling Set
on ProfileService
won’t trigger our other components to re-render.
For that we need to raise an event…
public class ProfileService
{
public event Action OnChange;
public Profile Profile { get; private set; }
public async Task Set(Profile profile)
{
Profile = profile;
OnChange?.Invoke();;
}
public async Task SetIfNull(Profile profile)
{
if (Profile == null)
Profile = profile;
}
}
Now we have something we can react to when the profile gets updated.
Back in UserName
we can subscribe to this event.
@using ChildComponents.State
@inject ProfileService ProfileService
@implements IDisposable
@if (ProfileService.Profile != null)
{
<h1>Welcome @ProfileService.Profile.Name !</h1>
}
@code
{
protected override void OnInitialized()
{
ProfileService.OnChange += StateHasChanged;
}
public void Dispose()
{
ProfileService.OnChange -= StateHasChanged;
}
}
This registers StateHasChanged
as a handler for the OnChange
event and removes it again when the component is disposed.
Now, every time the profile is updated the OnChange
event will be fired and UserName
re-rendered to show the latest data.
Pros of centralised state
- Makes it possible for multiple parts of your application to react to state changes
- Gives you one place to inspect the overall state of your application (if you adopt one of the libraries which store all your UI state in a single store)
- Makes it easier to minimises network calls to fetch data
Cons of centralised state
- Increases the complexity of your app (more moving parts)
- Something else to learn (Flux architecture depending on which library you choose)
- Probably overkill for many simple apps
- “Rolling your own” starts simple enough but quickly increases in complexity as your app grows
In summary
Simple apps only need simple state management!
The three primary options for getting state into your Blazor components are:
- Let each component fetch its own state
- Pass state in via parameters
- Use cascading values
But if these options aren’t enough, and if managing the state of your UI starts to feel like a battle, consider a centralised store for your UI state.
Once you go down this road you can use one of the various Blazor state management libraries which are available, or roll your own “simple” equivalent.