Imagine for a moment you are filling out the world's longest form. You've spent 30 minutes entering detailed information from your address to your date of birth to a list of the last seven countries you visited. You click the “submit” button and are immediately rewarded with a “The connection has been lost” message. No worries, right? Simply click the back button and … oh, no! The form is empty. You sound your barbaric yalp and pledge never to revisit the site again.
This is not the experience you want for your website visitors. Therefore, it is important to understand how to manage state in Blazor apps. Managing state while minimizing the amount of code you must write to manage state? “Yes, please!”
First, let's be clear on what we mean by “state” in a Blazor app. For the best possible user experience, it's important to provide a consistent experience to the end user when their connection is temporarily lost and when they refresh or navigate back to the page. The components of this experience include:
The HTML Document Object Model (DOM) that represents the user interface (UI)
The fields and properties representing the data being input and/or output on the page
The state of registered services that are running as part of code for the page
In the absence of any special code, state is maintained in two places depending on the Blazor hosting model. For Blazor WebAssembly (client-side) apps, state is held in browser memory until the user refreshes or navigates away from the page. In Blazor Server apps, state is held in special “buckets” allocated to each client session known as circuits. These circuits can lose state when they time out after a disconnection and may be obliterated even during an active connection when the server is under memory pressure.
The Reference App
To illustrate the nuances of state, I started with the Blazor Health App:
Build a sample app in Blazor, a .NET-based framework for building web apps that run in the browser and leverages C# and Razor templates to generate cross-platform, HTML5-compliant WebAssembly code.
I extended it to include two pages to illustrate some nuances of navigation. In the related GitHub repository:
This project is intended to illustrate how to better manage state in Blazor applications. It is based on the official documentation: ASP.NET Core Blazor state management.
To get started, fork the repo (optional) then clone it.
The project contains several variations of a Blazor app for tracking health statistics. (For the background behind the sample app, read: From Angular to Blazor: The Health App). This version follows a layered architecture:
The BlazorState.ViewModel project can be shared across .NET Core apps including Xamarin and WPF.
The BlazorState.Shared Razor class library contains views, styles, and JavaScript code that can be shared between Blazor WebAssembly and Blazor Server projects. If you're…
There are several sample projects. The problem manifests differently in Blazor WebAssembly and Blazor Server projects.
State in Blazor WebAssembly
In Blazor WebAssembly (client projects) the state is held in memory. This means a refresh or forced navigation will destroy state. To see this in action:
Set BlazorState.Wasm as the startup project and run it.
Update the form information.
Navigate to “results” and verify the same results exist.
Navigate back to “home” and force a refresh (usually CTRL+F5). Note the form reverts to defaults.
Update the form information, then manually navigate by adding /results to the URL bar in your browser and press ENTER. Note it also uses defaults.
Not a great experience! With Blazor Server, it's slightly different.
State in Blazor Server
Change the startup project to BlazorState.Server and run that project. Try the same steps you did for the client version and note the state is maintained because it is held in the server memory. While the app is open, stop and restart the web server. You should see a disconnect message. After the server comes back up, click the “reload” option and note that although the app recovers, it loses all its state.
Now we have a problem. Let's work on the solution!
Solutions Architecture
The following solutions uses an approach to architecture designed to maximize reuse. The Blazor.ViewModel project hosts an interface, properties and business logic for the app. It is a .NET Standard library implementation of the Model-View-ViewModel (MVVM) pattern that can be comfortably referenced by any type of .NET Core project, from WPF to Xamarin and even Blazor. Maximum reuse!
For UI and user experience logic, as well as shareable assets such as images, stylesheets, JavaScript code and even Razor view components, Blazor.Shared takes advantage of Razor Class Libraries. The solution implements a HealthModelBase to avoid duplicate MVVM code. It also implements all the state management solutions described here as services and/or components that are easily applied to both Blazor WebAssembly and Blazor Server projects. This further maximizes code reuse, as the “host” projects simply provide some structure to reference the share components and resources.
Now that I've covered the problem and the approach for solutions, let's move on to managing state in our Blazor app!
Service Registration
The first step may not be so obvious, but for the sake of being thorough I want to address services. To see this in action, create a new Blazor client app and run it. The built-in template provides a few pages with simple navigation. Navigate to the Counter page and increment the counter. Now, navigate away from the page and come back. The counter resets to zero! This is because the state of the counter is held in the component, so it is reset each time the component is initialized:
The service will persist in memory when the components are destroyed/recreated and maintain a consistent count even when you navigate. This is the first step to maintain state. The reference app registers the main viewmodel in this fashion.
Browser Cache
One option to maintain state is to take advantage of the browser cache using HTML5 Web Storage. The API is very simple. The stateManagement.js file in BlazorState.Shared defines a simple, globally accessible interface. It uses the localStorage JavaScript API, but you may choose to use sessionStorage instead.
This is included in the root index.html for Blazor WebAssembly projects and _Host.cshtml for Blazor Server projects. Including shared assets is as simple as using the path:
Blazor's component model makes it simple to create a “wrapper” component that manages the state changes. This is implemented in StorageHelper.razor. First, the using statements reference the viewmodel, JavaScript interoperability, and the JSON serializer. The implementations are injected.
In Blazor Server, the components are pre-rendered on the server. JavaScript is not available, so the interop call will throw an InvalidOperationException. This is caught the first time. The second call happens from the client and will succeed if the viewmodel is cached. After the JSON for the viewmodel is loaded from cache, it is deserialized and the properties are moved to the global viewmodel instance.
If the properties on the viewmodel change, the viewmodel is serialized and stored in the cache. This is skipped when the property change was fired because of the initial load (hence the isDeserializing flag, otherwise it will serialize while trying to deserialize). Now the component is ready for use! Both Blazor.ServerLocal and Blazor.WasmLocal use the helper, and it is implemented the same way in App.razor:
<BlazorState.Shared.StorageHelper><RouterAppAssembly="@typeof(Program).Assembly"><FoundContext="routeData"><RouteViewRouteData="@routeData"DefaultLayout="@typeof(MainLayout)"/></Found><NotFound><LayoutViewLayout="@typeof(MainLayout)"><p>Sorry, there's nothing at this address.</p></LayoutView></NotFound></Router></BlazorState.Shared.StorageHelper>
By wrapping the router, the state management handles all pages and components in the app without having to write additional code. You can open the browser developer tools and navigate to the application “local storage” to watch the values change as you update the form.
It is important to note that the user can access their local cache, so if you are storing sensitive values, they should be encrypted. An example of this is provided by the Microsoft.ASpNetCore.ProtectedBrowserStorage package.
Server-side Management
Another way to handle state is by calling an API and persisting it on the server. How it is persisted is up to you: options range from SQL, NoSQL to simple caches like Redis. BlazorState.WasmRemote.Server is an ASP.NET hosted Blazor WebAssembly app. The StateController exposes an API that stores and retrieves the viewmodel using the remote IP address as a key. This is done to keep the demo simple; a production application with authentication would likely key to a user and/or session.
The StateService in Blazor.Shared handles making the API calls. The constructor takes in the global viewmodel instance, an instance of IStateServiceConfig that provides the URL of the API endpoint, and an instance of HttpClient. It is important to inject HttpClient rather than create a new instance because Blazor WebAssembly requires a version that is specifically configured to run in the browser sandbox. The constructor registers for property changed notifications from the viewmodel.
InitAsync is called by page components during initialization to load the viewmodel state.
The code is very similar to the client cache approach but retrieves the model from the API call rather than the local cache. The property change handler serializes and posts the model to the server:
Set BlazorState.WasmRemote.Server as the startup project and run it to see this in action. You may need to update the correct URL (as the port may be different) in the Startup.cs implementation of IStateServiceConfig in the .Client project. With the solution running, open the network tab and note the calls as you update the form.
The service is demonstrated for Blazor WebAssembly but will work the same for Blazor Server.
Conclusion
Blazor is not opinionated about how you manage state. The services and component model make it easy to implement project-wide solutions. This post focused on an implementation of the Model-View-ViewModel pattern and registered for property changed notifications to handle serializing state either locally or over an API. The same approach will work if you are using a different approach such as Redux. The important steps are to update your store when properties mutate and load from your state management solution when components initialize. The rest is browser history!