In an application, you often need to store user settings such as the selected theme, application configuration, or the user's display name. These settings must be:
- Accessible from anywhere in the application
- Persisted, so you can read them when the user restarts the application
- Shared across multiple instances (tabs/windows in the context of a Blazor application)
In a WebAssembly application that runs inside a browser, you must use the storage provided by the browser. There are multiple locations where you can store data:
- Local storage:
- Contains only strings
- Limited to about 5MB
- Supports change notifications via the storage event
- IndexedDB database:
- Store key-value pairs with structured values
- Storage size is virtually unlimited
- Asynchronous API, so it doesn't block the UI
- The API is complex to use
- Cookies:
- Limited in size (about 4k)
- Sent with every matching HTTP request, which can increase request overhead
- File and Directory Entries API:
- Non-standard
- Write files to the filesystem
- Requires user consent
The easiest solution is to use local storage. Settings are typically small enough to fit within the size limit, and you can serialize data to JSON to satisfy the string-only requirement. It also supports change notifications via the storage event, allowing you to reload data whenever another tab or window updates the storage.
First, add the following JS functions to wwwroot/index.html to wrap the local storage API:
HTML
<script>
function BlazorSetLocalStorage(key, value) {
localStorage.setItem(key, value);
}
function BlazorGetLocalStorage(key) {
return localStorage.getItem(key);
}
function BlazorRegisterStorageEvent(component) {
window.addEventListener("storage", async e => {
await component.invokeMethodAsync("OnStorageUpdated", e.key);
});
}
</script>
Next, add a service to manage saving to storage. To avoid requiring a save button in the UI, the UserSettings class implements INotifyPropertyChanged. This allows settings to be saved automatically whenever a property value changes. The provider also reloads settings when the local storage data changes in another tab or window.
C#
public sealed class UserSettingsProvider
{
private const string KeyName = "state";
private readonly IJSRuntime _jsRuntime;
private bool _initialized;
private UserSettings _settings;
public event EventHandler Changed;
public bool AutoSave { get; set; } = true;
public UserSettingsProvider(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public async ValueTask<UserSettings> Get()
{
if (_settings != null)
return _settings;
// Register the Storage event handler. This handler calls OnStorageUpdated when the storage changed.
// This way, you can reload the settings when another instance of the application (tab / window) save the settings
if (!_initialized)
{
// Create a reference to the current object, so the JS function can call the public method "OnStorageUpdated"
var reference = DotNetObjectReference.Create(this);
await _jsRuntime.InvokeVoidAsync("BlazorRegisterStorageEvent", reference);
_initialized = true;
}
// Read the JSON string that contains the data from the local storage
UserSettings result;
var str = await _jsRuntime.InvokeAsync<string>("BlazorGetLocalStorage", KeyName);
if (str != null)
{
result = System.Text.Json.JsonSerializer.Deserialize<UserSettings>(str) ?? new UserSettings();
}
else
{
result = new UserSettings();
}
// Register the OnPropertyChanged event, so it automatically persists the settings as soon as a value is changed
result.PropertyChanged += OnPropertyChanged;
_settings = result;
return result;
}
public async Task Save()
{
var json = System.Text.Json.JsonSerializer.Serialize(_settings);
await _jsRuntime.InvokeVoidAsync("BlazorSetLocalStorage", KeyName, json);
}
// Automatically persist the settings when a property changed
private async void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (AutoSave)
{
await Save();
}
}
// This method is called from BlazorRegisterStorageEvent when the storage changed
[JSInvokable]
public void OnStorageUpdated(string key)
{
if (key == KeyName)
{
// Reset the settings. The next call to Get will reload the data
_settings = null;
Changed?.Invoke(this, EventArgs.Empty);
}
}
}
// The class that stores the user settings
public class UserSettings : INotifyPropertyChanged
{
private string username;
public string Username
{
get => username; set
{
username = value;
RaisePropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
You now need to edit the Program.cs file to register the service:
C#
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<UserSettingsProvider>();
await builder.Build().RunAsync();
}
You can now use the service in any component. However, duplicating the logic to load settings and react to the Changed event in every component is tedious. A cleaner approach is to encapsulate this logic in a dedicated component and use cascading parameters to pass the value to child components. Using StateHasChanged, the component can then refresh all children when the settings change. Create a new component Shared/UserSettingsComponent.razor:
Razor
@inject UserSettingsProvider UserSettingsProvider
@implements IDisposable
@if (state == null)
{
<p>loading...</p>
}
else
{
<CascadingValue Value="@state" IsFixed="false">@ChildContent</CascadingValue>
}
@code{
private UserSettings state = null;
[Parameter]
public RenderFragment ChildContent { get; set; }
protected override async Task OnInitializedAsync()
{
UserSettingsProvider.Changed += UserSettingsChanged;
await Refresh();
}
public void Dispose()
{
UserSettingsProvider.Changed -= UserSettingsChanged;
}
private async void UserSettingsChanged(object sender, EventArgs e)
{
await InvokeAsync(async () =>
{
await Refresh();
StateHasChanged();
});
}
private async Task Refresh()
{
state = await UserSettingsProvider.Get();
}
}
#How to get user settings in your components?
Modify App.razor to wrap the Router component with UserSettingsComponent:
Razor
<UserSettingsComponent>
<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>
</UserSettingsComponent>
You can now use the cascading parameter in any component:
Razor
@page "/"
<div class="form-group">
<label for="exampleInputEmail1">Username</label>
<input type="text" @bind="@State.Username" class="form-control" />
</div>
@code {
[CascadingParameter]
public UserSettings State { get; set; }
}
After validating the input, you should see the value in the local storage:

If you open multiple tabs, you should also see that the settings are reloaded automatically across instances:
#Additional resources
Do you have a question or a suggestion about this post? Contact me!