In the last post we saw how you can use TailwindCSS with Blazor to toggle “dark mode” on and off in your web applications.

But it’s a pain for your users to have to do this every time. It would be much nicer if this setting was persisted in some way. That way, they’d get the same “mode” each and every time they visit your site.

Considering Blazor for your next project?

Try this simple, repeatable process for transforming ideas into features using Blazor's component model.

4 days; 4 emails; enter your email in the box below and I'll send you lesson #1.

I respect your email privacy. No spam, unsubscribe anytime with one click.

 

Local Storage to the rescue

The simplest way to persist preferences like these is in the browser itself, using local storage.

Local Storage is part of the Web Storage API, available in all modern browsers, which enables the storage of key/value pairs.

In javascript you can read data from Local Storage using window.localStorage.

For example:

const name = window.localStorage.getItem('name');

And store values using the setItem method:

window.localStorage.setItem('name', 'Jon');

You can also store entire objects, you just have to serialise them to a string first…

window.localStorage.setItem('userInfo', JSON.stringify({ name: 'Jon' }));

Then deserialize them from the stored string on the way back out…

const rawUserInfo = window.localStorage.getItem('userInfo');
const userInfo = JSON.parse(rawUserInfo);
console.log(userInfo.name);

Now we could use Javascript Interop from our Blazor app to interact with local storage but it’s much more convenient to use the handy Blazored.LocalStorage NuGet package from Chris Sainty.

Follow the instructions here to add and configure Blazored.LocalStorage for your app.

Before we can think about using Local Storage to persist our preferences, we need to do a little refactoring on the implementation we ended up with last time.

Here’s ThemeToggle.Razor as it currently stands…

<button @onclick="Toggle">Toggle</button>

@code {

    private bool _darkMode = false;

    [Parameter]
    public EventCallback<bool> OnDarkModeToggled { get; set; }

    private void Toggle()
    {
        _darkMode = !_darkMode;
        OnDarkModeToggled.InvokeAsync(_darkMode);
    }
}

Note how we’re storing a value for _darkMode directly in the component, but we’re also storing it somewhere else too, in MainLayout.razor.


<!-- markup here -->

@code
{
    private bool _darkMode = false;
    
    private void HandleDarkModeToggled(bool isDarkMode)
    {
        _darkMode = isDarkMode;        
    }
}

So looking at this again, we’re effectively caching the boolean value for whether DarkMode is in use or not in two places.

If we were to then store this in LocalStorage too we’d be holding that value in three places.

This feels like overkill, and will likely require us to jump through unnecessary hoops to keep them all in sync.

In this case I’d lean towards consolidating that state into one place, so we can implement our business rules and logic (such as defaulting to the OS preference or value stored in Local Storage) without fear of repeating ourselves throughout the application.

Refactor shared state into one place (one source of truth)

Let’s start by creating a simple ProfileService and a Preferences record to store the DarkMode preference itself…

ProfileService.cs

public record Preferences
{
    public bool DarkMode { get; init; } 
}

public class ProfileService {

    public async Task ToggleDarkMode(){

    }

    public async Task<Preferences> GetPreferences(){

    }

}

The idea is we’ll call ToggleDarkMode to turn Dark Mode on or off, and we can retrieve the current preference using GetPreferences.

Let’s start by implementing ToggleDarkMode.

I’ll make use of C#‘s shiny new record feature here (because, well… you know… it’s new, and shiny!) and with expressions to flip Dark Mode on and off…

ProfileService.cs

public async Task ToggleDarkMode()
{
    var preferences = await GetPreferences();
    var newPreferences = preferences 
        with { DarkMode = !preferences.DarkMode }; 
}

This gives us a new instance of the Preferences record with DarkMode set to the opposite of what it was before.

But what are we to do with this new preferences instance? This is where we can lean on LocalStorage to persist our new preferences.

If we inject ILocalStorageService (from Blazored.LocalStorage) into our ProfileService we can use it to persist these preferences to Local Storage.

ProfileService.cs

public class ProfileService {

    private readonly ILocalStorageService _localStorageService;

    public ProfileService(ILocalStorageService localStorageService)
    {
        _localStorageService = localStorageService;
    }

    public async Task ToggleDarkMode(){
        var preferences = await GetPreferences();
        var newPreferences = preferences 
            with { DarkMode = !preferences.DarkMode };

        await _localStorageService.SetItemAsync("preferences", newPreferences);
    }

    public async Task<Preferences> GetPreferences(){
        return await _localStorageService.GetItemAsync<Preferences>("preferences") 
            ?? new Preferences();
    }
}

Now Dark Mode will be toggled on or off, then persisted to Local Storage.

Naturally, we want to be able to retrieve these preferences too so I’ve also implemented GetPreferences to look the preferences up from Local Storage, defaulting to a new instance of Preferences if nothing has yet been saved.

Register ProfileService for use via dependency injection

Now is also a good time to make sure that ProfileService is registered as a service so we can use it wherever we see fit. How you do this depends on whether you’re using Blazor WASM or Blazor Server.

In this case I’m using Blazor WASM so I need to head over to Program.cs and register the service there:

public static async Task Main(string[] args)
{
    var builder = WebAssemblyHostBuilder.CreateDefault(args);
    builder.RootComponents.Add<App>("#app");

    builder.Services.AddScoped(
        sp => new HttpClient {BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)});

    builder.Services.AddScoped<ProfileService>();
    builder.Services.AddBlazoredLocalStorage();
    
    await builder.Build().RunAsync();
}

Access shared state directly from Blazor components

So far so good, but we’re not actually using this ProfileService anywhere. To do that we’ll need to make a small tweak to…

ThemeToggle.Razor

@inject ProfileService ProfileService

<button @onclick="Toggle">Toggle</button>

@code {

    private void Toggle()
    {
        ProfileService?.ToggleDarkMode();
    }
}

We’ve removed the local boolean for _darkMode and replaced it with a direct call to Toggle Dark Mode by invoking ProfileService.ToggleDarkMode().

Note we’ve also removed the OnDarkModeToggled EventCallback parameter, because it’s now redundant.

We don’t want to wait until the user has tried to toggle Dark mode to read these preferences. They may have already switched to dark mode during a previous visit, so we want to be able to read their preferences and apply them whether or not they click the button to toggle dark mode on/off.

We need to fetch this state when the app is rendered for the first time, not just when the user clicks that button. For this reason we don’t need that event callback anymore and will make sure to retrieve the relevant state elsewhere.

Inject services into your Blazor components

You can inject a service directly into a Blazor components using this syntax:

@inject UserService UserService

This leans on .NET’s dependency injection system. So long as the service is registered, you can inject it into your components, then use it as you see fit.

Which brings us to the next challenge; how do we retrieve the preferences from ProfileService in order to actually switch themes?

For that we need to head over to MainLayout.razor.

As a reminder, here’s how it currently looks:

@inherits LayoutComponentBase

<div class="flex flex-col h-screen @(_darkMode ? "dark" : "")">

    <div class="bg-gray-600 p-4 flex justify-between">
        <h1 class="text-white">Blazor WASM Demo App</h1>
        <ThemeToggle OnDarkModeToggled="@HandleDarkModeToggled"/>
    </div>

    <div class="flex-grow flex dark:bg-gray-700">
        <div class="bg-gray-100 px-8 w-48 py-4 border-r shadow-md dark:bg-gray-500">
            <NavMenu/>
        </div>

        <div class="flex-grow w-auto p-8 mx-auto max-w-5xl">
            @Body
        </div>
    </div>

</div>

@code
{
    private bool _darkMode = false;
    
    private void HandleDarkModeToggled(bool isDarkMode)
    {
        _darkMode = isDarkMode;        
    }
}

The task here is to bring in our ProfileService and use that to retrieve preferences, then use those preferences to drive whether dark mode is enabled or not.

@inherits LayoutComponentBase
@inject ProfileService ProfileService

<div class="flex flex-col h-screen @(_preferences.DarkMode ? "dark" : "")">

    <div class="bg-gray-600 p-4 flex justify-between">
        <h1 class="text-white">Blazor WASM Demo App</h1>
        <ThemeToggle/>
    </div>

    <div class="flex-grow flex dark:bg-gray-700">
        <div class="bg-gray-100 px-8 w-48 py-4 border-r shadow-md dark:bg-gray-500">
            <NavMenu/>
        </div>

        <div class="flex-grow w-auto p-8 mx-auto max-w-5xl">
            @Body
        </div>
    </div>

</div>

@code
{
    private Preferences _preferences = new();

    protected override async Task OnInitializedAsync()
    {
        _preferences = await ProfileService.GetPreferences();
    }    
}

We’ve made a couple of changes here:

<ThemeToggle /> no longer takes an OnDarkModeToggled EventCallback Parameter so we’ve removed the code which assigned that.

We retrieve the user’s preferences from ProfileService during OnInitializedAsync and store the preferences in a field which our markup then reads to determine whether to switch to Dark Mode or not.

Avoid trying to access LocalStorage from OnInitializedAsync if using Blazor Server

We need to be mindful of one limitation with using Local Storage this way from a Blazor app.

If you’re using Blazor WASM this code will work just fine, but if you’re using Blazor Server you can’t access LocalStorage via javascript during OnInitialized.

This is because that code runs on the server as part of a prerendering phase, and the server doesn’t have access to your browser’s local storage via javascript (kinda makes sense when you think about it!)

In this case you can achieve the same result, but you’ll need to move the code to OnAfterRenderAsync.

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        _preferences = await ProfileService.GetPreferences();
        StateHasChanged();
    }
}

OnAfterRenderAsync does not run as part of the prerendering phase on the server, only running once the app is loaded and rendered in the browser.

At this point we can check if this is the firstRender, and if so retrieve the preferences from local storage, assign them to the _preferences field then explicitly instruct Blazor that StateHasChanged so it knows it needs to render again (typically it wouldn’t need to render again after rendering has just finished, hence the need to be explicit).

It’s important to include that firstRender test because without that we’d get into an infinite loop and all sorts of shenanigans would occur.

Tell Blazor to re-render

We’re almost there, but run this now, click the button and…

… nothing happens!

Well actually, something does happen, but the theme doesn’t change.

Crack open the developer tools, hit the Application tab (in Chrome/Edge) and you’ll see the local storage being set as you click the button….

But Blazor isn’t re-rendering, and for good reason… we haven’t told it to.

When we click to the toggle button it calls ProfileService which in turn updates Local Storage, but we never tell the UI to re-render and there’s nothing in place to make this happen automatically.

This is where we can use good old C# events to make sure we can react and trigger a render when preferences have changed.

Here’s an updated version of ProfileService which invokes an event every time preferences are changed.

 public class ProfileService
{
    private readonly ILocalStorageService _localStorageService;

    public event Action<Preferences> OnChange;

    public ProfileService(ILocalStorageService localStorageService)
    {
        _localStorageService = localStorageService;
    }

    public async Task ToggleDarkMode()
    {
        var preferences = await GetPreferences();
        var newPreferences = preferences with {DarkMode = !preferences.DarkMode};
        await _localStorageService.SetItemAsync("preferences", newPreferences);

        OnChange?.Invoke(newPreferences);
    }

    public async Task<Preferences> GetPreferences()
    {       
        return await _localStorageService.GetItemAsync<Preferences>("preferences") 
            ?? new Preferences();   
    }
}

With this we get a handy event which will be raised each and every time Dark mode is toggled (on or off).

Now we just need to hook into this event in MainLayout.razor to trigger a re-render of our component.

@inherits LayoutComponentBase
@inject ProfileService ProfileService
@implements IDisposable

//markup omitted for brevity

@code
{
    private Preferences _preferences = new();

    protected override async Task OnInitializedAsync()
    {
        ProfileService.OnChange += ProfileServiceOnOnChange;
        _preferences = await ProfileService.GetPreferences();
    }

    private void ProfileServiceOnOnChange(Preferences newPreferences)
    {
        _preferences = newPreferences;
        StateHasChanged();
    }

    public void Dispose()
    {
        ProfileService.OnChange -= ProfileServiceOnOnChange;
    }

}

We register ProfileServiceOnChange as a handler for ProfileService.OnChange. The result is this method will be invoked every time OnChange is invoked by code in ProfileService.

Because the event includes the new preferences as an argument it’s easy to grab those and update the preferences in MainLayout accordingly.

We have to call StateHasChanged so Blazor knows to re-render at this point.

Note we’ve also implemented IDisposable and made to sure to unregister the event handler as well. This is good practice to make sure we’re cleaning up after ourselves!

With that we can click the button to toggle dark mode, knowing that we’ll get the new theme we want and the preferences will be stored in local storage for next time.

Respect their OS preferences

One last thing to consider, your visitors may have set dark mode as their preference for apps at OS level.

Here’s the relevant Windows setting.

It’s pretty straightforward to read this setting using javascript…

window.matchMedia('(prefers-color-scheme: dark)').matches;

If this returns true then they’ve selected dark mode as their preference.

We can access this from Blazor via JS interop…

First we need to write the javascript, in this case we can just add it to index.html (for Blazor WASM) or _host.cshtml (for Blazor Server).

index.html

<body>
    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>

<script>
    window.prefersDarkMode = function(){
        return window.matchMedia('(prefers-color-scheme: dark)').matches;
    }
</script>

</body>

Now ProfileService can read this when deciding which theme to initially prefer…

public class ProfileService
{
    private readonly ILocalStorageService _localStorageService;
    private readonly IJSRuntime _jsRuntime;

    public event Action<Preferences> OnChange;

    public ProfileService(
        ILocalStorageService localStorageService, 
        IJSRuntime jsRuntime)
    {
        _jsRuntime = jsRuntime;
        _localStorageService = localStorageService;
    }

    public async Task ToggleDarkMode()
    {
        var preferences = await GetPreferences();
        var newPreferences = preferences with {DarkMode = !preferences.DarkMode};
        await _localStorageService.SetItemAsync("preferences", newPreferences);

        OnChange?.Invoke(newPreferences);
    }

    public async Task<Preferences> GetPreferences()
    {
        // if they've already specified their preferences explicitly, use them
        if (await _localStorageService.ContainKeyAsync("preferences"))
            return await _localStorageService.GetItemAsync<Preferences>("preferences");

        // else default to OS settings...
        var prefersDarkMode = await _jsRuntime.InvokeAsync<bool>("prefersDarkMode");

        return new Preferences
        {
            DarkMode = prefersDarkMode
        };
    }
}

In summary

When you need to store a user’s preferences consider using Local Storage.

To avoid keeping state in multiple places you can use a service to handle getting and setting state, this is also a good place to put any business logic you need to be consistent across multiple use cases. There are other solutions to this challenge of storing state in Blazor which might also be worth a look…

If you need to re-render based on state changes, consider using an event and event callback to react to state changes and re-render your Blazor components accordingly.

Build better ASP.NET web applications, faster

I email every week with hints, tips and advice on how to push through all the noise to get your ASP.NET applications built.

Drop your email in the box below and I'll send new articles straight to your inbox.

I respect your email privacy. No spam, unsubscribe anytime with one click.

 

Next up

Will .NET 6 fix Blazor Prerendering?
Prerendering eradicates Blazor WASM’s initial load time and .NET 6 promises to address its one key limitation
Dark mode for your web applications (using Blazor and Tailwind CSS)
Eyestrain is a real problem; help your users by adapting your site to their dark mode preferences
Is it possible to render components “dynamically” using Blazor?
Because life’s not complicated enough already!