Imagine you are filling out the longest form in the world. You’ve spent 30 minutes entering details, from your address to your birthday, to a list of seven countries you’ve visited recently. You click the “Submit” button, and immediately get a “Connection Lost” message. No worries, right? Just click the “Back” button… oh no! The form is empty. This feels brutal, and you vow never to visit the site again.
This is not the experience you want for your website visitors. So it’s important to understand how to manage state in Blazor applications. Managing state while minimizing the amount of code you have to write to manage it? “Yes, please!”
Watch the related video: State Management in Blazor Apps
1. Definition of Blazor State
First, let’s clarify what “state” means in a Blazor application. To provide the best user experience, it’s important to deliver a consistent experience for the end user when their connection is temporarily interrupted, when they refresh, or when they navigate back to the page. The components of that experience include:
- The HTML Document Object Model (DOM) that represents the user interface (UI).
- The fields and properties that represent data being entered and/or output on the page.
- The state of registered services that run as part of the page’s code.
Without any special code, state is held in two places depending on the Blazor hosting model. For Blazor WebAssembly (client-side) applications, state is kept in browser memory until the user refreshes or navigates away from the page. In Blazor Server applications, state is held in special “buckets” assigned to each client session called circuits. These circuits may lose state when they time out after disconnection, or even during an active connection if the server is under memory pressure.
2. Reference Application
To illustrate the nuances of state, I started with the Blazor Health App:
From Angular to Blazor: The Health App

Building the sample application in Blazor, a .NET-based framework for building web applications that run in the browser, using C# and Razor templates to generate cross-platform, HTML5-compliant WebAssembly code.
I extended it to include two pages to illustrate some navigation nuances. In the related GitHub repository:
There are several sample projects. The problems manifest differently in Blazor WebAssembly and Blazor Server projects.
3. State in Blazor WebAssembly
In Blazor WebAssembly (client-side project), state is held in memory. This means a refresh or forced navigation destroys the state. To see this in action:
- Set
BlazorState.Wasmas the startup project and run it. - Update the form information.
- Navigate to “Results” and verify the same results are present.
- Navigate back to “Home” and force a refresh (usually
CTRL+F5). Notice the form reverts to defaults. - Update the form information again, then manually navigate by adding
/resultsto the browser’s URL bar and pressingENTER. Notice it also uses defaults.
Bad experience! It’s slightly different with Blazor Server.
4. State in Blazor Server
Change the startup project to BlazorState.Server and run it. Try the same steps as with the client version, and note that state is preserved because it is stored in server memory. Once the application is open, stop and restart the web server. You should see a disconnection message. After the server restarts, click the “Reload” option and note that even though the application resumes, it loses all state.
Now we have a problem. Let’s look into the solutions!
5. Solution Architecture
The following solution uses an architectural approach designed to maximize reusability. The Blazor.ViewModel project hosts the application’s interfaces, properties, and business logic. It is a .NET Standard library implementation of the Model-View-ViewModel (MVVM) pattern that can be easily referenced from any type of .NET Core project, including WPF, Xamarin, or even Blazor. Maximum reusability!
For UI and user experience logic, as well as shareable assets like images, stylesheets, JavaScript code, and even Razor view components, Blazor.Shared leverages a Razor Class Library. The solution implements HealthModelBase to avoid duplicating MVVM code. It also implements all the state management solutions described here as services and/or components that can be easily applied to both Blazor WebAssembly and Blazor Server projects. Because the “host” project only provides minimal structure to reference the shared components and resources, this further maximizes code reuse.

Now that I’ve covered the problem and the approach to solutions, let’s move on to managing state in Blazor applications!
6. Service Registration
The first step might not be so obvious, but for completeness, I want to introduce services. To see this in action, create a new Blazor client application and run it. The built-in template provides simple navigation for a few pages. Navigate to the Counter page and increment the counter. Now leave the page and navigate back. The counter resets to zero! This is because the counter’s state is held in the component, so it is reset every time the component is initialized:
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
To maintain “in-memory” (or Blazor Server “circuit”) state, you can create a “counter service”:
public class CounterService
{
public int Count { get; private set; }
public void Increment()
{
Count += 1;
}
}
Register the service in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<CounterService, CounterService>();
}
…then remove the code in the @code block of Counter.razor, inject the counter service, and bind directly:
@inject CounterService Svc
<h1>Counter</h1>
<p>Current count: @Svc.Count</p>
<button class="btn btn-primary" @onclick="Svc.Increment">Click me</button>
When the component is destroyed/recreated, the service remains in memory, maintaining a consistent count even during navigation. This is the first step in maintaining state. The reference application registers the main view model this way.
7. Browser Caching
One way to maintain state is to leverage browser caching using HTML5 Web Storage. The API is straightforward. The stateManagement.js file in BlazorState.Shared defines a simple, globally accessible interface. It uses the localStorage JavaScript API, but you could choose to use sessionStorage.
window.stateManager = {
save: function (key, str) {
localStorage[key] = str;
},
load: function (key) {
return localStorage[key];
}
};
It is included in the root of index.html for Blazor WebAssembly projects and _Host.cshtml for Blazor Server projects. Including shared assets is as simple as using the path:
<script src="_content/BlazorState.Shared/stateManagement.js"></script>
Blazor’s component model makes it simple to create a “wrapper” component that manages state changes. This is implemented in StorageHelper.razor. First, using statements reference the view model, JavaScript interop, and JSON serializer. The implementations are injected.
@using Microsoft.JSInterop; @using System.Text.Json; @inject IJSRuntime JsRuntime @inject IHealthModel Model
The template simply wraps child components and renders them when the state has loaded.
@if (hasLoaded) { @ChildContent } else {
<p>Loading...</p>
}
When the component is initialized, the code attempts to load the view model from cache:
string vm;
try
{
vm = await JsRuntime.InvokeAsync<string>("stateManager.load", nameof(HealthModel));
}
catch(InvalidOperationException)
{
return;
}
In Blazor Server, the component is pre-rendered on the server. JavaScript is not available, so the interop call throws an InvalidOperationException. This is caught the first time. The second call is from the client, and if the viewmodel is cached, it succeeds. After loading the viewmodel’s JSON from cache, it is deserialized and properties are moved to the global viewmodel instance.
var viewModel = JsonSerializer.Deserialize<HealthModel>(vm);
if (viewModel != null)
{
isDeserializing = true;
Model.AgeYears = viewModel.AgeYears;
Model.HeightInches = viewModel.HeightInches;
Model.IsFemale = viewModel.IsFemale;
Model.IsImperial = viewModel.IsImperial;
Model.WeightPounds = viewModel.WeightPounds;
isDeserializing = false;
}
The isDeserializing flag is important to avoid infinite loops, as seen in the next code that registers property change notifications:
Model.PropertyChanged += async (o, e) =>
{
if (isDeserializing)
{
return;
}
var vmStr = JsonSerializer.Serialize(((HealthModel)Model));
await JsRuntime.InvokeAsync<object>(
"stateManager.save", nameof(HealthModel), vmStr);
};
hasLoaded = true;
If a property on the viewmodel changes, the viewmodel is serialized and stored in cache. This is skipped when property changes are triggered by the initial load (hence the isDeserializing flag, otherwise it would serialize while trying to deserialize). Now the component is ready to use! Both Blazor.ServerLocal and Blazor.WasmLocal use it in the same way in App.razor:
<BlazorState.Shared.StorageHelper>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</BlazorState.Shared.StorageHelper>
By wrapping the router, state management handles all pages and components in the application without writing additional code. You can open browser developer tools and navigate to the application’s “Local Storage” to observe values changing as the form is updated.

It’s important to note that users can access their local cache, so if you are storing sensitive values, you should encrypt them. The Microsoft.AspNetCore.ProtectedBrowserStorage package provides an example.
8. Server-Side Management
Another way to handle state is to call an API and persist it on the server. How persistent it is depends on you: options range from SQL, NoSQL, to simple caching like Redis. BlazorState.WasmRemote.Server is an ASP.NET-hosted Blazor WebAssembly application. 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 lock to a user and/or session.
The StateService in Blazor.Shared handles the API calls. The constructor accepts a global viewmodel instance, an IStateServiceConfig instance that provides the API endpoint URL, and an HttpClient instance. It is important to inject HttpClient instead of creating a new instance because Blazor WebAssembly requires a version specifically configured to run in the browser sandbox. The constructor registers property change notifications from the viewmodel.
The InitAsync method is called by page components during initialization to load the viewmodel state.
public async Task InitAsync()
{
_initializing = true;
var vmJson = await _client.GetStringAsync(_config.Url);
var vm = JsonSerializer.Deserialize<HealthModel>(vmJson, _options);
_model.AgeYears = vm.AgeYears;
_model.HeightInches = vm.HeightInches;
_model.IsFemale = vm.IsFemale;
_model.IsMetric = vm.IsMetric;
_model.WeightPounds = vm.WeightPounds;
_initializing = false;
}
This code is very similar to the client caching approach, but retrieves the model from an API call rather than local cache. The property change handler serializes the model and posts it to the server:
private async void Model_PropertyChanged(object sender,
System.ComponentModel.PropertyChangedEventArgs e)
{
if (_initializing || _config == null)
{
return;
}
var vm = JsonSerializer.Serialize(_model);
var content = new StringContent(vm);
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
await _client.PostAsync(_config.Url, content);
}
Set BlazorState.WasmRemote.Server as the startup project and run it to see it in action. You may need to update the correct URL in the IStateServiceConfig implementation in the .Client project’s Startup.cs (because ports may differ). With the solution running, open the “Network” tab and note the calls as you update the form.

The service was demonstrated for Blazor WebAssembly, but it works the same for Blazor Server.
9. Conclusion
Blazor has no opinion on how you manage state. The service and component models make it easy to implement project-wide solutions. This article focused on an implementation of the Model-View-ViewModel pattern, registering property change notifications to handle serialized state either locally or via an API. If you use a different approach like Redux, the same method will work. The important steps are to update the store when properties change and load from the state management solution when components initialize. The rest is browser history!
Check out the official documentation on ASP.NET Core Blazor state management.